1use crate::{
17 Client,
18 client::ClientError,
19 context::XmtpSharedContext,
20 groups::{GroupError, MlsGroup, UpdateGroupMembershipResult, group_permissions::PolicySet},
21 identity_updates::{
22 IdentityUpdates, get_association_state_for_client, get_association_state_with_verifier,
23 load_identity_updates, load_identity_updates_for_client,
24 },
25};
26use futures::future::try_join_all;
27use std::collections::HashMap;
28use xmtp_api::{ApiClientWrapper, XmtpApi};
29use xmtp_common::{ErrorCode, RetryableError};
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_mls_common::{
40 group::{DMMetadataOptions, GroupMetadataOptions},
41 group_metadata::DmMembers,
42};
43use xmtp_proto::{types::ApiIdentifier, xmtp::identity::associations::IdentifierKind};
44
45#[cfg(test)]
46mod tests;
47
48pub const HANDLE_SEARCH_CODES: usize = 100;
51
52const HANDLE_CODE_COUNT: usize = 10_000;
54
55#[derive(Debug, thiserror::Error, ErrorCode)]
57pub enum HandleError {
58 #[error(transparent)]
59 #[error_code(inherit)]
60 Invalid(#[from] InvalidHandle),
61 #[error("no inbox holds the handle")]
65 NotFound,
66 #[error("the inbox has made the most handle claims it can")]
69 LimitReached,
70}
71
72impl RetryableError for HandleError {
73 fn is_retryable(&self) -> bool {
74 false
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum IdentityTarget {
83 Account(Identifier),
84 Handle(Handle),
85}
86
87pub(crate) fn ensure_claim_allowed(state: &AssociationState) -> Result<(), HandleError> {
91 if state.handle_claims() >= MAX_HANDLE_CLAIMS {
92 return Err(HandleError::LimitReached);
93 }
94 Ok(())
95}
96
97fn handle_lookup(handle: &Handle) -> ApiIdentifier {
99 ApiIdentifier {
100 identifier: handle.as_str().to_owned(),
101 identifier_kind: IdentifierKind::Handle,
102 }
103}
104
105async fn lookup_handles<A: XmtpApi>(
108 api: &ApiClientWrapper<A>,
109 handles: &[Handle],
110) -> Result<Vec<Option<InboxId>>, ClientError> {
111 if handles.is_empty() {
112 return Ok(Vec::new());
113 }
114 Ok(api
115 .get_inbox_ids(handles.iter().map(handle_lookup).collect())
116 .await?)
117}
118
119fn answered_inboxes(answers: &[Option<InboxId>]) -> Vec<InboxId> {
121 let mut inboxes: Vec<InboxId> = answers.iter().flatten().cloned().collect();
122 inboxes.sort_unstable();
123 inboxes.dedup();
124 inboxes
125}
126
127fn state_if_logged(
130 result: Result<AssociationState, ClientError>,
131) -> Result<Option<AssociationState>, ClientError> {
132 match result {
133 Ok(state) => Ok(Some(state)),
134 Err(ClientError::Association(AssociationError::MissingIdentityUpdate)) => Ok(None),
135 Err(error) => Err(error),
136 }
137}
138
139fn keep_verified(
144 handles: &[Handle],
145 answers: Vec<Option<InboxId>>,
146 states: &HashMap<InboxId, AssociationState>,
147) -> Vec<Option<InboxId>> {
148 handles
149 .iter()
150 .zip(answers)
151 .map(|(handle, answer)| {
152 answer.filter(|inbox_id| {
153 states
154 .get(inbox_id)
155 .is_some_and(|state| state.handles().contains(handle))
156 })
157 })
158 .collect()
159}
160
161pub(crate) async fn resolve_handles_in<Context: XmtpSharedContext>(
165 context: &Context,
166 handles: &[Handle],
167) -> Result<Vec<Option<InboxId>>, ClientError> {
168 let answers = lookup_handles(context.api(), handles).await?;
169 let inboxes = answered_inboxes(&answers);
170 if inboxes.is_empty() {
171 return Ok(answers);
172 }
173 let conn = context.db();
174 let inbox_refs: Vec<&str> = inboxes.iter().map(String::as_str).collect();
175 load_identity_updates_for_client(context, &conn, &inbox_refs).await?;
176 let states = try_join_all(inboxes.into_iter().map(|inbox_id| async {
177 let state = get_association_state_for_client(context, &conn, &inbox_id, None).await;
178 Ok::<_, ClientError>(state_if_logged(state)?.map(|state| (inbox_id, state)))
179 }))
180 .await?;
181 let states: HashMap<InboxId, AssociationState> = states.into_iter().flatten().collect();
182 Ok(keep_verified(handles, answers, &states))
183}
184
185pub async fn resolve_handles_with_verifier<A: XmtpApi>(
188 api: &ApiClientWrapper<A>,
189 conn: &impl DbQuery,
190 handles: &[Handle],
191 verifier: &impl SmartContractSignatureVerifier,
192) -> Result<Vec<Option<InboxId>>, ClientError> {
193 let answers = lookup_handles(api, handles).await?;
194 let inboxes = answered_inboxes(&answers);
195 if inboxes.is_empty() {
196 return Ok(answers);
197 }
198 let inbox_refs: Vec<&str> = inboxes.iter().map(String::as_str).collect();
199 load_identity_updates(api, conn, &inbox_refs).await?;
200 let mut states = HashMap::new();
201 for inbox_id in inboxes {
202 let state = get_association_state_with_verifier(conn, &inbox_id, None, verifier).await;
203 if let Some(state) = state_if_logged(state)? {
204 states.insert(inbox_id, state);
205 }
206 }
207 Ok(keep_verified(handles, answers, &states))
208}
209
210pub(crate) async fn resolve_targets_in<Context: XmtpSharedContext>(
216 context: &Context,
217 targets: &[IdentityTarget],
218) -> Result<Vec<Option<InboxId>>, ClientError> {
219 let handles: Vec<Handle> = targets
220 .iter()
221 .filter_map(|target| match target {
222 IdentityTarget::Handle(handle) => Some(handle.clone()),
223 IdentityTarget::Account(_) => None,
224 })
225 .collect();
226 let mut handle_inboxes = resolve_handles_in(context, &handles).await?.into_iter();
227 let accounts: Vec<ApiIdentifier> = targets
228 .iter()
229 .filter_map(|target| match target {
230 IdentityTarget::Account(identifier) => Some(identifier.into()),
231 IdentityTarget::Handle(_) => None,
232 })
233 .collect();
234 let mut account_inboxes = if accounts.is_empty() {
235 Vec::new()
236 } else {
237 context.api().get_inbox_ids(accounts).await?
238 }
239 .into_iter();
240 targets
241 .iter()
242 .map(|target| match target {
243 IdentityTarget::Handle(_) => handle_inboxes
244 .next()
245 .flatten()
246 .map(Some)
247 .ok_or_else(|| HandleError::NotFound.into()),
248 IdentityTarget::Account(_) => Ok(account_inboxes.next().flatten()),
249 })
250 .collect()
251}
252
253fn require_account_inboxes(
256 targets: &[IdentityTarget],
257 inboxes: Vec<Option<InboxId>>,
258) -> Result<Vec<InboxId>, GroupError> {
259 let missing: Vec<String> = targets
260 .iter()
261 .zip(&inboxes)
262 .filter_map(|(target, inbox)| match (target, inbox) {
263 (IdentityTarget::Account(identifier), None) => Some(identifier.to_string()),
264 _ => None,
265 })
266 .collect();
267 if !missing.is_empty() {
268 return Err(GroupError::AddressNotFound(missing));
269 }
270 Ok(inboxes.into_iter().flatten().collect())
271}
272
273fn all_accounts(targets: &[IdentityTarget]) -> Option<Vec<Identifier>> {
275 targets
276 .iter()
277 .map(|target| match target {
278 IdentityTarget::Account(identifier) => Some(identifier.clone()),
279 IdentityTarget::Handle(_) => None,
280 })
281 .collect()
282}
283
284fn search_candidates(handle: Handle) -> Vec<Handle> {
288 if handle.name() != handle.as_str() {
289 return vec![handle];
290 }
291 let codes = rand::seq::index::sample(
292 &mut xmtp_common::rng(),
293 HANDLE_CODE_COUNT,
294 HANDLE_SEARCH_CODES,
295 );
296 let coded: Vec<Handle> = codes
297 .into_iter()
298 .map(|code| {
299 let code = u16::try_from(code).expect("a sampled code is below 10 000");
300 Handle::with_code(handle.name(), code)
301 .expect("a name part with a four-digit code is a handle")
302 })
303 .collect();
304 std::iter::once(handle).chain(coded).collect()
305}
306
307impl<Context: XmtpSharedContext> Client<Context> {
308 pub async fn resolve_handles(
313 &self,
314 handles: &[Handle],
315 ) -> Result<Vec<Option<InboxId>>, ClientError> {
316 resolve_handles_in(&self.context, handles).await
317 }
318
319 pub async fn handles_have_owner(&self, handles: &[Handle]) -> Result<Vec<bool>, ClientError> {
323 Ok(lookup_handles(self.context.api(), handles)
324 .await?
325 .iter()
326 .map(Option::is_some)
327 .collect())
328 }
329
330 pub async fn available_handles(&self, input: &str) -> Result<Vec<Handle>, ClientError> {
339 let handle = Handle::from_input(input).map_err(HandleError::from)?;
340 let candidates = search_candidates(handle);
341 let owned = self.handles_have_owner(&candidates).await?;
342 Ok(candidates
343 .into_iter()
344 .zip(owned)
345 .filter_map(|(candidate, owned)| (!owned).then_some(candidate))
346 .collect())
347 }
348
349 pub async fn claim_handle(&self, handle: &Handle) -> Result<(), ClientError> {
355 self.ensure_identity_ready()?;
356 let identity_updates = IdentityUpdates::new(&self.context);
357 let conn = self.context.db();
358 load_identity_updates_for_client(&self.context, &conn, &[self.inbox_id()]).await?;
359 let state = identity_updates
360 .get_association_state(&conn, self.inbox_id(), None)
361 .await?;
362 ensure_claim_allowed(&state)?;
363 let request = identity_updates
364 .claim_handle_request(handle.clone())
365 .await?;
366 identity_updates.apply_signature_request(request).await
367 }
368
369 pub async fn resolve_targets(
373 &self,
374 targets: &[IdentityTarget],
375 ) -> Result<Vec<InboxId>, ClientError> {
376 let inboxes = resolve_targets_in(&self.context, targets).await?;
377 targets
378 .iter()
379 .zip(inboxes)
380 .map(|(target, inbox)| match (target, inbox) {
381 (_, Some(inbox)) => Ok(inbox),
382 (IdentityTarget::Account(identifier), None) => {
383 Err(NotFound::InboxIdForAddress(identifier.to_string()).into())
384 }
385 (IdentityTarget::Handle(_), None) => Err(HandleError::NotFound.into()),
386 })
387 .collect()
388 }
389
390 pub async fn find_dm_by_target(
393 &self,
394 target: &IdentityTarget,
395 ) -> Result<Option<MlsGroup<Context>>, ClientError> {
396 let peer = match target {
397 IdentityTarget::Account(identifier) => {
398 self.find_inbox_id_from_identifier(&self.context.db(), identifier.clone())
399 .await?
400 }
401 IdentityTarget::Handle(handle) => self
402 .resolve_handles(std::slice::from_ref(handle))
403 .await?
404 .pop()
405 .flatten(),
406 };
407 let Some(peer) = peer else {
408 return Ok(None);
409 };
410 let stored = self.context.db().find_active_dm_group(&DmMembers {
411 member_one_inbox_id: self.inbox_id(),
412 member_two_inbox_id: peer.as_str(),
413 })?;
414 stored.map(|stored| self.group(&stored.id)).transpose()
415 }
416
417 pub async fn find_or_create_dm_by_target(
420 &self,
421 target: &IdentityTarget,
422 opts: DMMetadataOptions,
423 ) -> Result<MlsGroup<Context>, ClientError> {
424 match target {
425 IdentityTarget::Account(identifier) => {
426 self.find_or_create_dm_by_identity(identifier.clone(), Some(opts))
427 .await
428 }
429 IdentityTarget::Handle(handle) => {
430 let peer = self
431 .resolve_handles(std::slice::from_ref(handle))
432 .await?
433 .pop()
434 .flatten()
435 .ok_or(HandleError::NotFound)?;
436 self.find_or_create_dm(peer, Some(opts)).await
437 }
438 }
439 }
440
441 pub async fn create_group_with_targets(
446 &self,
447 targets: &[IdentityTarget],
448 permissions_policy_set: Option<PolicySet>,
449 opts: Option<GroupMetadataOptions>,
450 ) -> Result<MlsGroup<Context>, ClientError> {
451 if let Some(accounts) = all_accounts(targets) {
452 return self
453 .create_group_with_identifiers(&accounts, permissions_policy_set, opts)
454 .await;
455 }
456 let inboxes = resolve_targets_in(&self.context, targets).await?;
457 let inbox_ids = require_account_inboxes(targets, inboxes)?;
458 self.create_group_with_members(&inbox_ids, permissions_policy_set, opts)
459 .await
460 }
461}
462
463impl<Context: XmtpSharedContext> MlsGroup<Context> {
464 pub async fn add_members_by_targets(
469 &self,
470 targets: &[IdentityTarget],
471 ) -> Result<UpdateGroupMembershipResult, GroupError> {
472 if let Some(accounts) = all_accounts(targets) {
473 return self.add_members_by_identity(&accounts).await;
474 }
475 let inboxes = resolve_targets_in(&self.context, targets).await?;
476 let inbox_ids = require_account_inboxes(targets, inboxes)?;
477 self.add_members(&inbox_ids).await
478 }
479
480 pub async fn remove_members_by_targets(
485 &self,
486 targets: &[IdentityTarget],
487 ) -> Result<(), GroupError> {
488 if let Some(accounts) = all_accounts(targets) {
489 return self.remove_members_by_identity(&accounts).await;
490 }
491 let inbox_ids: Vec<InboxId> = resolve_targets_in(&self.context, targets)
492 .await?
493 .into_iter()
494 .flatten()
495 .collect();
496 let inbox_ids: Vec<&str> = inbox_ids.iter().map(String::as_str).collect();
497 self.remove_members(&inbox_ids).await
498 }
499}