Skip to main content

xmtp_mls/
client.rs

1use crate::{
2    builder::DeviceSyncMode,
3    context::XmtpSharedContext,
4    groups::{
5        ConversationListItem, GroupError, MlsGroup, group_permissions::PolicySet,
6        welcome_sync::WelcomeService,
7    },
8    identity::{Identity, IdentityError, parse_credential},
9    identity_updates::{IdentityUpdateError, IdentityUpdates, load_identity_updates},
10    mls_store::{MlsStore, MlsStoreError},
11    subscriptions::{LocalEventError, LocalEvents, SyncWorkerEvent},
12    utils::VersionInfo,
13    worker::device_sync::{
14        DeviceSyncClient, preference_sync::PreferenceUpdate, worker::SyncMetric,
15    },
16    worker::{WorkerRunner, metrics::WorkerMetrics},
17};
18pub mod notifications;
19use crate::{
20    groups::welcome_sync::GroupSyncSummary,
21    identity_updates::{batch_get_association_state_with_verifier, get_creation_signature_kind},
22    messages::{
23        decoded_message::DecodedMessage,
24        enrichment::{EnrichMessageError, enrich_messages},
25    },
26};
27use itertools::Itertools;
28use openmls::prelude::tls_codec::Error as TlsCodecError;
29use std::{collections::HashMap, sync::Arc};
30use thiserror::Error;
31use tokio::sync::broadcast;
32use xmtp_api::{ApiClientWrapper, XmtpApi};
33use xmtp_common::{ErrorCode, Event, Retry, retry_async, retryable};
34use xmtp_configuration::{CREATE_PQ_KEY_PACKAGE_EXTENSION, KEY_PACKAGE_ROTATION_INTERVAL_NS};
35use xmtp_cryptography::signature::IdentifierValidationError;
36use xmtp_db::TransactionOutcome::Continue;
37use xmtp_db::{
38    ConnectionExt, NotFound, StorageError, TransactionOutcome, XmtpDb,
39    consent_record::{ConsentState, ConsentType, StoredConsentRecord},
40    db_connection::DbConnection,
41    encrypted_store::conversation_list::ConversationListItem as DbConversationListItem,
42    group::{ConversationType, GroupMembershipState, GroupQueryArgs},
43    group_message::StoredGroupMessage,
44    identity::StoredIdentity,
45    identity_cache::StoredIdentityKind,
46};
47use xmtp_db::{group::GroupQueryOrderBy, prelude::*};
48use xmtp_id::key_package::{KeyPackageVerificationError, VerifiedKeyPackageV2};
49use xmtp_id::{
50    AsIdRef, InboxId, InboxIdRef,
51    associations::{
52        AssociationError, AssociationState, Identifier, MemberIdentifier, SignatureError,
53        builder::{SignatureRequest, SignatureRequestError},
54    },
55    scw_verifier::SmartContractSignatureVerifier,
56};
57use xmtp_macro::log_event;
58use xmtp_mls_common::{
59    group::{DMMetadataOptions, GroupMetadataOptions},
60    group_metadata::DmMembers,
61    group_mutable_metadata::MessageDisappearingSettings,
62};
63use xmtp_proto::{
64    ConversionError,
65    api::HasStats,
66    api_client::{ApiStats, IdentityStats},
67};
68use xmtp_proto::{types::InstallationId, xmtp::identity::associations::IdentifierKind};
69
70use xmtp_proto::types::GroupId;
71/// Enum representing the network the Client is connected to
72#[derive(Clone, Copy, Default, Debug)]
73pub enum Network {
74    Local(&'static str),
75    #[default]
76    Dev,
77    Prod,
78}
79
80/// Timeout for waiting until a registration publish can be read.
81#[derive(Debug, Clone)]
82pub struct VisibilityConfirmationOptions {
83    pub timeout_ms: u64,
84}
85
86const REGISTRATION_INITIAL_BACKOFF: std::time::Duration = std::time::Duration::from_millis(50);
87const REGISTRATION_MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(1);
88
89impl Default for VisibilityConfirmationOptions {
90    fn default() -> Self {
91        Self { timeout_ms: 30_000 }
92    }
93}
94
95#[derive(Debug, Error, ErrorCode)]
96pub enum ClientError {
97    #[error(transparent)]
98    #[error_code(inherit)]
99    AddressValidation(#[from] IdentifierValidationError),
100    /// Could not publish.
101    ///
102    /// Failed to publish messages to the network. May be retryable.
103    #[error("could not publish: {0}")]
104    PublishError(String),
105    /// Storage error.
106    ///
107    /// Database operation failed. May be retryable.
108    #[error("storage error: {0}")]
109    Storage(#[from] StorageError),
110    /// API error.
111    ///
112    /// Network request to XMTP backend failed. Retryable.
113    #[error("API error: {0}")]
114    Api(#[from] xmtp_api::ApiError),
115    /// Identity error.
116    ///
117    /// Problem with identity operations. Not retryable.
118    #[error("identity error: {0}")]
119    Identity(#[from] crate::identity::IdentityError),
120    /// TLS Codec error.
121    ///
122    /// Encoding/decoding MLS TLS structures failed. Not retryable.
123    #[error("TLS Codec error: {0}")]
124    TlsError(#[from] TlsCodecError),
125    /// Key package verification failed.
126    ///
127    /// Invalid key package received from network. Not retryable.
128    #[error("key package verification: {0}")]
129    KeyPackageVerification(#[from] KeyPackageVerificationError),
130    /// Stream inconsistency.
131    ///
132    /// Message stream state became inconsistent. Not retryable.
133    #[error("Stream inconsistency error: {0}")]
134    StreamInconsistency(String),
135    /// Association error.
136    ///
137    /// Identity association operation failed. Not retryable.
138    #[error("Association error: {0}")]
139    Association(#[from] AssociationError),
140    /// Signature validation error.
141    ///
142    /// A signature failed verification. Not retryable.
143    #[error("signature validation error: {0}")]
144    SignatureValidation(#[from] SignatureError),
145    /// Identity update error.
146    ///
147    /// Failed to process identity update. Not retryable.
148    #[error(transparent)]
149    IdentityUpdate(#[from] IdentityUpdateError),
150    /// Signature request error.
151    ///
152    /// Failed to create/process signature request. Not retryable.
153    #[error(transparent)]
154    SignatureRequest(#[from] SignatureRequestError),
155    /// Group error.
156    ///
157    /// Group operation failed. May be retryable.
158    // the box is to prevent infinite cycle between client and group errors
159    #[error(transparent)]
160    Group(Box<GroupError>),
161    /// Local event error.
162    ///
163    /// Failed to process local event. Not retryable.
164    #[error(transparent)]
165    LocalEvent(#[from] LocalEventError),
166    /// Database connection error.
167    ///
168    /// Connection to database failed. Retryable.
169    #[error(transparent)]
170    Db(#[from] xmtp_db::ConnectionError),
171    /// Generic error.
172    ///
173    /// Unclassified error. May be retryable.
174    #[error("generic:{0}")]
175    Generic(String),
176    /// MLS store error.
177    ///
178    /// OpenMLS key store operation failed. Not retryable.
179    #[error(transparent)]
180    MlsStore(#[from] MlsStoreError),
181    /// Message enrichment error.
182    ///
183    /// Failed to enrich message content. Not retryable.
184    #[error(transparent)]
185    EnrichMessage(#[from] EnrichMessageError),
186    /// Conversion Error
187    ///
188    /// Data type failed to convert. Not retryable.
189    #[error(transparent)]
190    Conversion(#[from] xmtp_proto::ConversionError),
191    /// Registration not visible.
192    ///
193    /// Registration has no publish cursor or is not visible before the timeout. Not retryable.
194    #[error("Registration is not visible")]
195    RegistrationNotVisible,
196    /// Client is closed.
197    ///
198    /// Operation was attempted on a client that has been shut down via
199    /// `Client::close`. Not retryable — build a new client instead.
200    #[error("client is closed")]
201    AlreadyClosed,
202    /// Server configuration unavailable.
203    ///
204    /// The backend did not serve its configuration, or the answer could not be
205    /// stored. A backend older than spec 006 answers `UNIMPLEMENTED`; there is
206    /// no compatibility shim. Retryable exactly when the wrapped failure is:
207    /// an unreachable backend is worth another attempt, `UNIMPLEMENTED` is not.
208    #[error("server configuration unavailable: {0}")]
209    ConfigurationUnavailable(#[source] Box<crate::server_configuration::ConfigurationFetchError>),
210    /// Server configuration invalid.
211    ///
212    /// The backend published a configuration this client cannot use: a missing
213    /// or malformed identifier, a minimum version that does not parse, or a
214    /// chain that is not a CAIP-2 identifier. Not retryable.
215    #[error("server configuration invalid: {0}")]
216    ConfigurationInvalid(#[from] xmtp_configuration::ServerConfigurationError),
217    /// Backend mismatch.
218    ///
219    /// This database is bound to one backend deployment and a different one
220    /// answered. Not retryable — use a database created for the backend this
221    /// app now points at.
222    #[error("this database is bound to backend {stored}, but {received} answered")]
223    BackendMismatch { stored: String, received: String },
224    /// Client version too old.
225    ///
226    /// The backend requires a newer libxmtp than this build. Not retryable —
227    /// ship an updated client.
228    #[error("client version {client} is below the {minimum} this backend requires")]
229    ClientVersionTooOld { client: String, minimum: String },
230    /// Authentication required.
231    ///
232    /// The backend requires a credential and none was configured. Not
233    /// retryable — supply an auth callback or handle before building.
234    #[error(
235        "this backend requires authentication; required scopes: [{}]",
236        required_scopes.join(", ")
237    )]
238    AuthRequired { required_scopes: Vec<String> },
239    /// Chain not accepted.
240    ///
241    /// The backend does not verify smart contract wallet signatures on this
242    /// chain. Not retryable — use a chain the deployment accepts.
243    #[error(
244        "this backend does not accept smart contract wallet signatures on {chain}; accepted: [{}]",
245        accepted.join(", ")
246    )]
247    ChainNotAccepted {
248        chain: String,
249        accepted: Vec<String>,
250    },
251}
252
253impl ClientError {
254    pub fn db_needs_connection(&self) -> bool {
255        match self {
256            Self::Storage(s) => s.db_needs_connection(),
257            Self::Db(c) => c.db_needs_connection(),
258            _ => false,
259        }
260    }
261}
262
263impl From<NotFound> for ClientError {
264    fn from(value: NotFound) -> Self {
265        ClientError::Storage(StorageError::NotFound(value))
266    }
267}
268
269impl From<GroupError> for ClientError {
270    fn from(err: GroupError) -> ClientError {
271        ClientError::Group(Box::new(err))
272    }
273}
274
275impl xmtp_common::RetryableError for ClientError {
276    fn is_retryable(&self) -> bool {
277        match self {
278            ClientError::Group(group_error) => retryable!(group_error),
279            ClientError::Api(api_error) => retryable!(api_error),
280            ClientError::Storage(storage_error) => retryable!(storage_error),
281            ClientError::Db(db) => retryable!(db),
282            // SCW verification errors carry retryability through SignatureError;
283            // transient RPC provider failures must not advance the welcome cursor.
284            // See xmtp/libxmtp#3394.
285            ClientError::SignatureValidation(e) => retryable!(e),
286            // A backend that was briefly unreachable, or a database that was
287            // briefly locked, is worth another attempt: the refresh worker's
288            // backoff depends on this answer.
289            ClientError::ConfigurationUnavailable(e) => retryable!(e),
290            ClientError::Generic(err) => err.contains("database is locked"),
291            _ => false,
292        }
293    }
294}
295
296impl From<String> for ClientError {
297    fn from(value: String) -> Self {
298        Self::Generic(value)
299    }
300}
301
302impl From<&str> for ClientError {
303    fn from(value: &str) -> Self {
304        Self::Generic(value.to_string())
305    }
306}
307
308/// Clients manage access to the network, identity, and data store
309pub struct Client<Context> {
310    pub context: Context,
311    pub installation_id: InstallationId,
312    pub(crate) local_events: broadcast::Sender<LocalEvents>,
313    pub(crate) workers: Arc<WorkerRunner>,
314}
315
316impl<Context> Drop for Client<Context> {
317    fn drop(&mut self) {
318        log_event!(Event::ClientDropped, self.installation_id);
319    }
320}
321
322#[derive(Clone)]
323pub struct DeviceSync {
324    pub(crate) mode: DeviceSyncMode,
325}
326
327// most of these things are `Arc`'s
328impl<Context: Clone> Clone for Client<Context> {
329    fn clone(&self) -> Self {
330        Self {
331            context: self.context.clone(),
332            installation_id: self.installation_id,
333            local_events: self.local_events.clone(),
334            workers: self.workers.clone(),
335        }
336    }
337}
338
339impl<Context> Client<Context>
340where
341    Context: XmtpSharedContext,
342{
343    pub fn identity_updates(&self) -> IdentityUpdates<&Context> {
344        IdentityUpdates::new(&self.context)
345    }
346
347    pub fn mls_store(&self) -> MlsStore<Context> {
348        MlsStore::new(self.context.clone())
349    }
350
351    pub fn scw_verifier(&self) -> Arc<Box<dyn SmartContractSignatureVerifier>> {
352        self.context.scw_verifier()
353    }
354
355    pub fn version_info(&self) -> &VersionInfo {
356        self.context.version_info()
357    }
358}
359
360impl<Context> Client<Context>
361where
362    Context: XmtpSharedContext,
363    Context::ApiClient: HasStats,
364{
365    pub fn api_stats(&self) -> ApiStats {
366        self.context.api().api_client.mls_stats()
367    }
368
369    pub fn identity_api_stats(&self) -> IdentityStats {
370        self.context.api().api_client.identity_stats()
371    }
372
373    pub fn clear_stats(&self) {
374        self.context.api().api_client.mls_stats().clear();
375        self.context.api().api_client.identity_stats().clear();
376    }
377}
378
379/// Get the [`AssociationState`] for each `inbox_id`
380pub async fn inbox_addresses_with_verifier<ApiClient: XmtpApi>(
381    api_client: &ApiClientWrapper<ApiClient>,
382    conn: &impl DbQuery,
383    inbox_ids: Vec<InboxIdRef<'_>>,
384    scw_verifier: &impl SmartContractSignatureVerifier,
385) -> Result<Vec<AssociationState>, ClientError> {
386    load_identity_updates(api_client, conn, &inbox_ids).await?;
387    let state = batch_get_association_state_with_verifier(
388        conn,
389        &inbox_ids.into_iter().map(|i| (i, None)).collect::<Vec<_>>(),
390        scw_verifier,
391    )
392    .await?;
393    Ok(state)
394}
395
396impl<Context> Client<Context>
397where
398    Context: XmtpSharedContext + 'static,
399{
400    /// Reconnect to the client's database if it has previously been released
401    pub fn reconnect_db(&self) -> Result<(), ClientError> {
402        if self.context.is_closed() {
403            return Err(ClientError::AlreadyClosed);
404        }
405        self.context.db().reconnect().map_err(StorageError::from)?;
406        self.workers.spawn(self.context.clone());
407        Ok(())
408    }
409
410    /// Cleanly shut down this client: cancel in-flight workers and streams,
411    /// then release the DB connection. Idempotent — a second call is `Ok(())`.
412    ///
413    /// Callers (notably the Node binding consumers) should `await` this before
414    /// deleting the SQLite file or dropping the client wrapper, to avoid late
415    /// log spew from detached workers/streams firing against a dead DB.
416    pub async fn close(&self) -> Result<(), ClientError> {
417        // `shutdown_complete` is distinct from `is_closed()` (which reflects
418        // cancellation): only set after the DB actually disconnects. If
419        // `disconnect()` errors below, callers can retry `close()` and we'll
420        // attempt disconnect again rather than silently short-circuiting.
421        if self.context.shutdown_complete() {
422            return Ok(());
423        }
424        self.context.cancellation_token().cancel();
425        self.context.close_message_delivery()?;
426        self.workers.shutdown().await;
427        self.context
428            .db()
429            .disconnect()
430            .map_err(xmtp_db::StorageError::from)?;
431        self.context.mark_shutdown_complete();
432        log_event!(Event::ClientClosed, self.installation_id);
433        Ok(())
434    }
435
436    /// yields until the sync worker notifies that it is initialized and running.
437    pub async fn wait_for_sync_worker_init(&self) {
438        self.workers.wait_for_sync_worker_init().await;
439    }
440
441    pub fn sync_metrics(&self) -> Option<Arc<WorkerMetrics<SyncMetric>>> {
442        self.workers.sync_metrics()
443    }
444}
445
446impl<Context> Client<Context>
447where
448    Context: XmtpSharedContext,
449{
450    /// What this deployment published about itself, as resolved at build
451    /// (CFG-030, CFG-080). A refresh rewrites the stored copy; it never changes
452    /// this value.
453    pub fn server_configuration(&self) -> &xmtp_configuration::ServerConfiguration {
454        self.context.server_configuration().configuration()
455    }
456
457    /// Fetch the deployment configuration now and rewrite the stored copy
458    /// (CFG-082).
459    ///
460    /// Applies the same validation (CFG-044), storage (CFG-048), and identifier
461    /// binding (CFG-051) the refresh worker applies. The snapshot this client is
462    /// holding is unchanged: a new value takes effect at the next build.
463    pub async fn refresh_server_configuration(
464        &self,
465    ) -> Result<xmtp_configuration::ServerConfiguration, ClientError> {
466        let handle = self.context.server_configuration();
467        let db = self.context.db();
468        let fetched =
469            match crate::server_configuration::fetch_and_store(self.context.api(), &db, handle)
470                .await
471            {
472                Ok(fetched) => fetched,
473                Err(error) => {
474                    // CFG-051: a latch closes every open stream, and cancelling
475                    // is what closes them. The worker cancels after its turn;
476                    // an explicit refresh has to do it here, because the latch
477                    // it sets — a different deployment identifier — otherwise
478                    // leaves the streams and workers of a database known to
479                    // belong elsewhere still running.
480                    if handle.latched().is_some() {
481                        self.context.cancellation_token().cancel();
482                    }
483                    return Err(error);
484                }
485            };
486        if let Err(ClientError::ClientVersionTooOld { client, minimum }) =
487            crate::server_configuration::check_minimum_version(
488                &fetched,
489                self.context.version_info().pkg_semver().semver(),
490            )
491        {
492            tracing::error!(
493                %client,
494                %minimum,
495                "the backend now requires a newer libxmtp than this client"
496            );
497            // CFG-061: the copy is stored either way, and the client stops.
498            let error = handle.latch(
499                crate::server_configuration::ConfigurationLatch::ClientVersionTooOld {
500                    client,
501                    minimum,
502                },
503            );
504            self.context.cancellation_token().cancel();
505            return Err(error);
506        }
507        Ok(fetched)
508    }
509
510    /// Retrieves the client's installation public key, sometimes also called `installation_id`
511    pub fn installation_public_key(&self) -> InstallationId {
512        self.context.installation_id()
513    }
514    /// Retrieves the client's inbox ID
515    pub fn inbox_id(&self) -> InboxIdRef<'_> {
516        self.context.identity().inbox_id()
517    }
518
519    /// get a reference to the monolithic Database object where
520    /// higher-level queries are defined
521    pub fn db(&self) -> <Context::Db as XmtpDb>::DbQuery {
522        self.context.db()
523    }
524
525    /// This associates an installation_id with a human-readable
526    /// name and makes the logs a little easier to read.
527    #[cfg(any(test, feature = "test-utils"))]
528    pub fn set_name(&self, name: &str) {
529        log_event!(Event::AssociateName, self.context.installation_id(), name);
530    }
531
532    pub fn device_sync_client(&self) -> DeviceSyncClient<Context> {
533        let metrics = self.context.sync_metrics();
534        DeviceSyncClient::new(
535            self.context.clone(),
536            metrics.unwrap_or(Arc::new(WorkerMetrics::new(self.context.installation_id()))),
537        )
538    }
539
540    /// Calls the server to look up the `inbox_id` associated with a given identifier
541    pub async fn find_inbox_id_from_identifier(
542        &self,
543        conn: &impl DbQuery,
544        identifier: Identifier,
545    ) -> Result<Option<String>, ClientError> {
546        let results = self
547            .find_inbox_ids_from_identifiers(conn, &[identifier])
548            .await?;
549        Ok(results.into_iter().next().flatten())
550    }
551
552    /// Calls the server to look up the `inbox_id`s` associated with a list of identifiers.
553    /// If no `inbox_id` is found, returns None.
554    pub(crate) async fn find_inbox_ids_from_identifiers(
555        &self,
556        conn: &impl DbQuery,
557        identifiers: &[Identifier],
558    ) -> Result<Vec<Option<String>>, ClientError> {
559        let ids: Vec<(String, StoredIdentityKind)> = identifiers
560            .iter()
561            .map(|i| {
562                Ok::<_, ConversionError>((
563                    i.clone().to_string(),
564                    StoredIdentityKind::try_from(IdentifierKind::from(i))?,
565                ))
566            })
567            .try_collect()?;
568        let cached_inbox_ids = conn.fetch_cached_inbox_ids(&ids)?;
569        let mut new_inbox_ids: HashMap<&Identifier, Option<String>> = HashMap::new();
570
571        let missing: Vec<_> = identifiers
572            .iter()
573            .filter(|ident| !cached_inbox_ids.contains_key(&format!("{ident}")))
574            .collect();
575
576        if !missing.is_empty() {
577            let requests = missing
578                .iter()
579                .map(|identifier| (*identifier).into())
580                .collect();
581            let results = self.context.api().get_inbox_ids(requests).await?;
582            new_inbox_ids = missing.into_iter().zip(results).collect();
583        }
584
585        let inbox_ids = identifiers
586            .iter()
587            .map(|ident| {
588                let cache_key = format!("{ident}");
589                if let Some(inbox_id) = cached_inbox_ids.get(&cache_key) {
590                    return Some(inbox_id.clone());
591                }
592                new_inbox_ids.get(ident).cloned().flatten()
593            })
594            .collect();
595        Ok(inbox_ids)
596    }
597
598    /// Get the highest `sequence_id` from the local database for the client's `inbox_id`.
599    /// This may not be consistent with the latest state on the backend.
600    pub fn inbox_sequence_id(
601        &self,
602        conn: &DbConnection<<Context::Db as XmtpDb>::Connection>,
603    ) -> Result<i64, StorageError> {
604        self.context
605            .identity()
606            .sequence_id(conn)
607            .map_err(Into::into)
608    }
609
610    /// Get the [`AssociationState`] for the client's `inbox_id`
611    pub async fn inbox_state(
612        &self,
613        refresh_from_network: bool,
614    ) -> Result<AssociationState, ClientError> {
615        let conn = self.context.db();
616        let inbox_id = self.inbox_id();
617        if refresh_from_network {
618            load_identity_updates(self.context.api(), &conn, &[inbox_id]).await?;
619        }
620        let identity_service = IdentityUpdates::new(&self.context);
621        let state = identity_service
622            .get_association_state(&conn, inbox_id, None)
623            .await?;
624        Ok(state)
625    }
626
627    /// Get the [`AssociationState`] for each `inbox_id`
628    pub async fn inbox_addresses(
629        &self,
630        refresh_from_network: bool,
631        inbox_ids: Vec<InboxIdRef<'_>>,
632    ) -> Result<Vec<AssociationState>, ClientError> {
633        let conn = self.context.db();
634        if refresh_from_network {
635            load_identity_updates(self.context.api(), &conn, &inbox_ids).await?;
636        }
637        let identity_service = IdentityUpdates::new(&self.context);
638        let state = identity_service
639            .batch_get_association_state(
640                &conn,
641                &inbox_ids.into_iter().map(|i| (i, None)).collect::<Vec<_>>(),
642            )
643            .await?;
644        Ok(state)
645    }
646
647    /// Get the total number of inbox updates for `inbox_ids`. `refresh_from_network` will force
648    /// a network refresh. May still access network if an inbox_id does not yet exist in the local
649    /// cache.
650    pub async fn fetch_inbox_updates_count(
651        &self,
652        refresh_from_network: bool,
653        inbox_ids: Vec<InboxIdRef<'_>>,
654    ) -> Result<HashMap<InboxId, u32>, ClientError> {
655        let conn = self.context.db();
656        if refresh_from_network {
657            load_identity_updates(self.context.api(), &conn, &inbox_ids).await?;
658        }
659        let inbox_id_strs = inbox_ids.to_vec();
660        let counts = conn.count_inbox_updates(&inbox_id_strs)?;
661        Ok(counts.into_iter().map(|(k, v)| (k, v as u32)).collect())
662    }
663
664    /// Get the total number of inbox updates for the client's inbox_id.
665    /// Setting `refresh_from_network` forces a network refresh, otherwise
666    /// this operation is offline.
667    pub async fn fetch_own_inbox_updates_count(
668        &self,
669        refresh_from_network: bool,
670    ) -> Result<u32, ClientError> {
671        let inbox_id = self.inbox_id();
672        Ok(self
673            .fetch_inbox_updates_count(refresh_from_network, vec![inbox_id])
674            .await?
675            .get(inbox_id)
676            .copied()
677            .unwrap_or(0))
678    }
679
680    /// Get the signature kind used to create an inbox.
681    ///
682    /// # Arguments
683    /// * `inbox_id` - The inbox ID to check
684    /// * `refresh_from_network` - Whether to fetch updates from the network first
685    ///
686    /// # Returns
687    /// * `Some(SignatureKind)` - The signature kind used to create the inbox
688    /// * `None` - Inbox doesn't exist or creation info is unavailable
689    pub async fn inbox_creation_signature_kind(
690        &self,
691        inbox_id: InboxIdRef<'_>,
692        refresh_from_network: bool,
693    ) -> Result<Option<xmtp_id::associations::SignatureKind>, ClientError> {
694        let conn = self.context.db();
695
696        // Load the first identity update (creation update) for this inbox if requested
697        if refresh_from_network {
698            load_identity_updates(self.context.api(), &conn, &[inbox_id]).await?;
699        }
700
701        let verifier = self.context.scw_verifier();
702
703        let signature_kind = get_creation_signature_kind(&conn, verifier, inbox_id).await?;
704
705        Ok(signature_kind)
706    }
707
708    /// Set a consent record in the local database.
709    /// If the consent record is an address set the consent state for both the address and `inbox_id`
710    pub async fn set_consent_states(
711        &self,
712        records: &[StoredConsentRecord],
713    ) -> Result<(), ClientError> {
714        let conn = self.context.db();
715        let changed_records = conn.insert_or_replace_consent_records(records)?;
716
717        if !changed_records.is_empty() {
718            self.context.task_channels().wake_notifications();
719            let updates: Vec<_> = changed_records
720                .into_iter()
721                .map(PreferenceUpdate::Consent)
722                .collect();
723
724            // Broadcast the consent update changes
725            let _ = self
726                .local_events
727                .send(LocalEvents::PreferencesChanged(updates.clone()));
728            let _ = self
729                .context
730                .worker_events()
731                .send(SyncWorkerEvent::SyncPreferences(updates));
732        }
733
734        Ok(())
735    }
736
737    /// Get the consent state for a given entity
738    pub async fn get_consent_state(
739        &self,
740        entity_type: ConsentType,
741        entity: String,
742    ) -> Result<ConsentState, ClientError> {
743        let conn = self.context.db();
744        let record = conn.get_consent_record(entity, entity_type)?;
745
746        match record {
747            Some(rec) => Ok(rec.state),
748            None => Ok(ConsentState::Unknown),
749        }
750    }
751
752    /// Release the client's database connection
753    pub fn release_db_connection(&self) -> Result<(), ClientError> {
754        self.context
755            .db()
756            .disconnect()
757            .map_err(xmtp_db::StorageError::from)?;
758        Ok(())
759    }
760
761    /// Get a reference to the client's identity struct
762    pub fn identity(&self) -> &Identity {
763        self.context.identity()
764    }
765
766    /// Ensures identity is ready before performing operations.
767    /// Call `register_identity()` first if this fails.
768    fn ensure_identity_ready(&self) -> Result<(), ClientError> {
769        // CFG-051 and CFG-061: once latched, every later call fails with the
770        // reason. This is the gate every client-level operation already passes
771        // through, so the check costs nothing extra.
772        self.context.server_configuration().check()?;
773        if !self.identity().is_ready() {
774            tracing::warn!(
775                inbox_id = %self.inbox_id(),
776                "Operation attempted before register_identity() was called"
777            );
778            return Err(IdentityError::UninitializedIdentity.into());
779        }
780        Ok(())
781    }
782
783    /// Create a new group with the default settings
784    /// Applies a custom [`PolicySet`] to the group if one is specified
785    pub fn create_group(
786        &self,
787        permissions_policy_set: Option<PolicySet>,
788        opts: Option<GroupMetadataOptions>,
789    ) -> Result<MlsGroup<Context>, ClientError> {
790        self.ensure_identity_ready()?;
791
792        let group: MlsGroup<Context> = MlsGroup::create_and_insert(
793            self.context.clone(),
794            ConversationType::Group,
795            permissions_policy_set.unwrap_or_default(),
796            opts.unwrap_or_default(),
797            None,
798        )?;
799
800        log_event!(
801            Event::CreatedGroup,
802            self.context.installation_id(),
803            group_id = group.group_id
804        );
805
806        // notify streams of our new group
807        let _ = self
808            .local_events
809            .send(LocalEvents::NewGroup(group.group_id));
810
811        Ok(group)
812    }
813
814    /// Create a group with an initial set of members added
815    pub async fn create_group_with_identifiers(
816        &self,
817        account_identifiers: &[Identifier],
818        permissions_policy_set: Option<PolicySet>,
819        opts: Option<GroupMetadataOptions>,
820    ) -> Result<MlsGroup<Context>, ClientError> {
821        let group = self.create_group(permissions_policy_set, opts)?;
822
823        group.add_members_by_identity(account_identifiers).await?;
824
825        Ok(group)
826    }
827
828    #[tracing::instrument(level = "debug", skip_all, fields(size = inbox_ids.len()))]
829    pub async fn create_group_with_members(
830        &self,
831        inbox_ids: &[impl AsIdRef],
832        permissions_policy_set: Option<PolicySet>,
833        opts: Option<GroupMetadataOptions>,
834    ) -> Result<MlsGroup<Context>, ClientError> {
835        tracing::info!("creating group");
836        let group = self.create_group(permissions_policy_set, opts)?;
837
838        group.add_members(inbox_ids).await?;
839
840        Ok(group)
841    }
842
843    /// Create a new Direct Message with the default settings
844    #[tracing::instrument(level = "debug", skip_all)]
845    async fn create_dm_by_inbox_id(
846        &self,
847        target_inbox_id: InboxId,
848        opts: Option<DMMetadataOptions>,
849    ) -> Result<MlsGroup<Context>, ClientError> {
850        let group: MlsGroup<Context> = MlsGroup::create_dm_and_insert(
851            &self.context,
852            GroupMembershipState::Allowed,
853            target_inbox_id.clone(),
854            opts.unwrap_or_default(),
855            None,
856        )?;
857
858        log_event!(
859            Event::CreatedDM,
860            self.context.installation_id(),
861            group_id = group.group_id,
862            target_inbox = target_inbox_id
863        );
864        // notify any streams of the new group
865        let _ = self
866            .local_events
867            .send(LocalEvents::NewGroup(group.group_id));
868
869        group.add_members(&[target_inbox_id]).await?;
870
871        Ok(group)
872    }
873
874    /// Find or create a Direct Message with the default settings
875    pub async fn find_or_create_dm_by_identity(
876        &self,
877        target_identity: Identifier,
878        opts: Option<DMMetadataOptions>,
879    ) -> Result<MlsGroup<Context>, ClientError> {
880        self.ensure_identity_ready()?;
881        tracing::info!("finding or creating dm with address: {target_identity}");
882        let inbox_id = match self
883            .find_inbox_id_from_identifier(&self.context.db(), target_identity.clone())
884            .await?
885        {
886            Some(id) => id,
887            None => {
888                return Err(NotFound::InboxIdForAddress(target_identity.to_string()).into());
889            }
890        };
891
892        self.find_or_create_dm(inbox_id, opts).await
893    }
894
895    /// Find or create a Direct Message by inbox_id with the default settings
896    pub async fn find_or_create_dm(
897        &self,
898        inbox_id: impl AsIdRef,
899        opts: Option<DMMetadataOptions>,
900    ) -> Result<MlsGroup<Context>, ClientError> {
901        self.ensure_identity_ready()?;
902        let inbox_id = inbox_id.as_ref();
903        tracing::info!("finding or creating dm with inbox_id: {}", inbox_id);
904        let db = self.context.db();
905        let group = db.find_active_dm_group(&DmMembers {
906            member_one_inbox_id: self.inbox_id(),
907            member_two_inbox_id: inbox_id,
908        })?;
909
910        if let Some(group) = group {
911            return Ok(MlsGroup::new(
912                self.context.clone(),
913                group.id,
914                group.dm_id,
915                group.conversation_type,
916                group.created_at_ns,
917            ));
918        }
919        self.create_dm_by_inbox_id(inbox_id.to_string(), opts).await
920    }
921
922    /// Look up a group by its ID
923    ///
924    /// Returns a [`MlsGroup`] if the group exists, or an error if it does not
925    ///
926    pub fn group(&self, group_id: &GroupId) -> Result<MlsGroup<Context>, ClientError> {
927        MlsStore::new(self.context.clone())
928            .group(group_id)
929            .map_err(Into::into)
930    }
931
932    /// Look up a group by its ID while stitching DMs
933    ///
934    /// Returns a [`MlsGroup`] if the group exists, or an error if it does not
935    ///
936    pub fn stitched_group(&self, group_id: &GroupId) -> Result<MlsGroup<Context>, ClientError> {
937        let conn = self.context.db();
938        let stored_group = conn.fetch_stitched(group_id)?;
939        stored_group
940            .map(|g| {
941                MlsGroup::new(
942                    self.context.clone(),
943                    g.id,
944                    g.dm_id,
945                    g.conversation_type,
946                    g.created_at_ns,
947                )
948            })
949            .ok_or(NotFound::GroupById(*group_id))
950            .map_err(Into::into)
951    }
952
953    /// Find all the duplicate dms for this group
954    pub fn find_duplicate_dms_for_group(
955        &self,
956        group_id: &GroupId,
957    ) -> Result<Vec<MlsGroup<Context>>, ClientError> {
958        let (group, _) = MlsGroup::new_cached(self.context.clone(), group_id)?;
959        group.find_duplicate_dms()
960    }
961
962    /// Fetches the message disappearing settings for a given group ID.
963    ///
964    /// Returns `Some(MessageDisappearingSettings)` if the group exists and has valid settings,
965    /// `None` if the group or settings are missing, or `Err(ClientError)` on a database error.
966    pub fn group_disappearing_settings(
967        &self,
968        group_id: &GroupId,
969    ) -> Result<Option<MessageDisappearingSettings>, ClientError> {
970        let (group, _) = MlsGroup::new_cached(self.context.clone(), group_id)?;
971        Ok(group.disappearing_settings()?)
972    }
973
974    /**
975     * Look up a DM group by the target's inbox_id.
976     *
977     * Returns a [`MlsGroup`] if the group exists, or an error if it does not
978     */
979    pub fn dm_group_from_target_inbox(
980        &self,
981        target_inbox_id: String,
982    ) -> Result<MlsGroup<Context>, ClientError> {
983        let conn = self.context.db();
984
985        let group = conn
986            .find_active_dm_group(&DmMembers {
987                member_one_inbox_id: self.inbox_id(),
988                member_two_inbox_id: &target_inbox_id,
989            })?
990            .ok_or(NotFound::DmByInbox(target_inbox_id))?;
991        Ok(MlsGroup::new(
992            self.context.clone(),
993            group.id,
994            group.dm_id,
995            group.conversation_type,
996            group.created_at_ns,
997        ))
998    }
999
1000    /// Look up a message by its ID
1001    /// Returns a [`StoredGroupMessage`] if the message exists, or an error if it does not
1002    pub fn message(&self, message_id: Vec<u8>) -> Result<StoredGroupMessage, ClientError> {
1003        let conn = &mut self.context.db();
1004        let message = conn.get_group_message(&message_id)?;
1005        Ok(message.ok_or(NotFound::MessageById(message_id))?)
1006    }
1007
1008    /// Look up and enrich a message by its ID, returning a [`DecodedMessage`]
1009    /// Returns an error if the message is not found or if it cannot be decoded/enriched
1010    #[xmtp_common::mls_span]
1011    pub fn message_v2(&self, message_id: Vec<u8>) -> Result<DecodedMessage, ClientError> {
1012        let conn = self.context.db();
1013        let message = conn
1014            .get_group_message(&message_id)?
1015            .ok_or_else(|| NotFound::MessageById(message_id.clone()))?;
1016
1017        let group_id = message.group_id;
1018
1019        let enriched = enrich_messages(conn, &group_id, vec![message])?;
1020
1021        // Since enrich_messages returns a Vec<DecodedMessage>, we can use .into_iter().next().ok_or(...) to take ownership without cloning.
1022        enriched
1023            .into_iter()
1024            .next()
1025            // In practice `enrich_messages` should always return an array of the same length as the input
1026            .ok_or_else(|| ClientError::Generic("Failed to decode message".to_string()))
1027    }
1028
1029    /// Delete a message by its ID
1030    /// This method is idempotent and will not error if the message is not found
1031    /// Returns the number of messages deleted (0 or 1)
1032    pub fn delete_message(&self, message_id: Vec<u8>) -> Result<usize, ClientError> {
1033        let conn = self.context.db();
1034
1035        // Fetch the message before deleting so we can emit the decoded message in the event
1036        let msg = conn.get_group_message(&message_id)?;
1037
1038        let num_deleted = conn.delete_message_by_id(&message_id)?;
1039        // Fire a local event if the message was successfully deleted
1040        if num_deleted > 0
1041            && let Some(message) = msg
1042        {
1043            let _ =
1044                self.context
1045                    .local_events()
1046                    .send(crate::subscriptions::LocalEvents::MsgsDeleted(vec![
1047                        message,
1048                    ]));
1049        }
1050
1051        Ok(num_deleted)
1052    }
1053
1054    /// Query for groups with optional filters
1055    ///
1056    /// Filters:
1057    /// - allowed_states: only return groups with the given membership states
1058    /// - created_after_ns: only return groups created after the given timestamp (in nanoseconds)
1059    /// - created_before_ns: only return groups created before the given timestamp (in nanoseconds)
1060    /// - limit: only return the first `limit` groups
1061    pub fn find_groups(&self, args: GroupQueryArgs) -> Result<Vec<MlsGroup<Context>>, ClientError> {
1062        MlsStore::new(self.context.clone())
1063            .find_groups(args)
1064            .map_err(Into::into)
1065    }
1066
1067    pub fn list_conversations(
1068        &self,
1069        args: GroupQueryArgs,
1070    ) -> Result<Vec<ConversationListItem<Context>>, ClientError> {
1071        let mut args = args.clone();
1072        // Default to last activity order by for this endpoint
1073        if args.order_by.is_none() {
1074            args.order_by = Some(GroupQueryOrderBy::LastActivity);
1075        }
1076        Ok(self
1077            .context
1078            .db()
1079            .fetch_conversation_list(args)?
1080            .into_iter()
1081            .map(|conversation_item: DbConversationListItem| {
1082                let message = conversation_item.message_id.and_then(|message_id| {
1083                    // Only construct StoredGroupMessage if all fields are Some
1084                    let msg: Option<StoredGroupMessage> = Some(StoredGroupMessage {
1085                        id: message_id,
1086                        group_id: conversation_item.id,
1087                        decrypted_message_bytes: conversation_item.decrypted_message_bytes?,
1088                        sent_at_ns: conversation_item.sent_at_ns?,
1089                        sender_installation_id: conversation_item.sender_installation_id?,
1090                        sender_inbox_id: conversation_item.sender_inbox_id?,
1091                        kind: conversation_item.kind?,
1092                        delivery_status: conversation_item.delivery_status?,
1093                        content_type: conversation_item.content_type?,
1094                        version_major: conversation_item.version_major?,
1095                        version_minor: conversation_item.version_minor?,
1096                        authority_id: conversation_item.authority_id?,
1097                        reference_id: None, // conversation_item does not use message reference_id
1098                        sequence_id: conversation_item.sequence_id?,
1099                        envelope_hash: None,
1100                        expiry_ns: None,
1101                        expire_at_ns: None, //Question: do we need to include this in conversation last message?
1102                        inserted_at_ns: 0, // Not used for conversation list display
1103                        should_push: true, // Not used for conversation list display
1104                        // The conversation_list view does not carry the key; use
1105                        // the timestamp proxy (display-only, never republished).
1106                        idempotency_key: conversation_item.sent_at_ns.unwrap_or_default().to_string(),
1107                    });
1108                    if msg.is_none() {
1109                        tracing::warn!("tried listing message, but message had missing fields so it was skipped");
1110                    }
1111                    msg
1112                });
1113
1114                ConversationListItem {
1115                    group: MlsGroup::new(
1116                        self.context.clone(),
1117                        conversation_item.id,
1118                        conversation_item.dm_id,
1119                        conversation_item.conversation_type,
1120                        conversation_item.created_at_ns,
1121                    ),
1122                    last_message: message,
1123                    is_commit_log_forked: conversation_item.is_commit_log_forked,
1124                }
1125            })
1126            .collect())
1127    }
1128
1129    /// Upload the key package before the identity update exposes this installation.
1130    /// Record its receipt for key retirement and retain the registration cursor.
1131    #[xmtp_common::mls_span]
1132    pub async fn register_identity(
1133        &self,
1134        signature_request: SignatureRequest,
1135    ) -> Result<(), ClientError> {
1136        tracing::info!("registering identity");
1137        // CFG-051 and CFG-061: registration is a network call like any other.
1138        self.context.server_configuration().check()?;
1139
1140        // Handle crash recovery - if already registered, just mark ready and return
1141        let stored_identity: Option<StoredIdentity> = self.context.db().fetch(&())?;
1142        if stored_identity.is_some() {
1143            tracing::info!("Identity already registered, skipping");
1144            self.identity().set_ready();
1145            return Ok(());
1146        }
1147
1148        // Step 1: Generate key package and store locally (not uploaded yet)
1149        let (kp_bytes, history_id) = self.identity().generate_and_store_key_package(
1150            self.context.mls_storage(),
1151            CREATE_PQ_KEY_PACKAGE_EXTENSION,
1152        )?;
1153
1154        // Step 2: Validate signatures (fails here if invalid - no network pollution)
1155        let identity_update = signature_request
1156            .build_identity_update()
1157            .map_err(IdentityUpdateError::from)?;
1158        identity_update
1159            .to_verified(&self.context.scw_verifier())
1160            .await?;
1161
1162        // Step 3: Upload key package first (prevents race condition)
1163        let key_package_meta = self.context.api().upload_key_package(kp_bytes).await?;
1164        let key_package_cursor = key_package_meta
1165            .cursor
1166            .filter(|cursor| cursor.sequence_id > 0)
1167            .ok_or(xmtp_api::ApiError::InvalidResponse(
1168                "key package publish cursor",
1169            ))?;
1170
1171        // Step 4: Publish identity update (makes installation visible)
1172        let registration_cursor = crate::identity_updates::publish_with_conflict_retry(
1173            self.context.api(),
1174            &self.context.db(),
1175            identity_update,
1176            &self.context.scw_verifier(),
1177        )
1178        .await?;
1179
1180        // Step 5: Fetch and store in local DB (needed for group operations)
1181        let inbox_id = self.inbox_id().to_string();
1182        retry_async!(
1183            Retry::default(),
1184            (async {
1185                load_identity_updates(self.context.api(), &self.context.db(), &[inbox_id.as_str()])
1186                    .await
1187            })
1188        )?;
1189
1190        // Backend publication order can differ from local generation order.
1191        crate::state_tx::state_write(self.context.mls_storage(), |tx| {
1192            let storage = tx.storage();
1193            storage.db().record_key_package_publication(
1194                history_id,
1195                xmtp_proto::types::Cursor(key_package_cursor.sequence_id),
1196            )?;
1197            storage
1198                .db()
1199                .reset_key_package_rotation_queue(KEY_PACKAGE_ROTATION_INTERVAL_NS)?;
1200            Ok::<_, StorageError>(Continue(()))
1201        })
1202        .map(TransactionOutcome::into_continued)?;
1203
1204        // Mark identity as ready
1205        let mut stored_identity = StoredIdentity::try_from(self.identity())?;
1206        stored_identity.registration_cursor_sequence_id = Some(registration_cursor.0 as i64);
1207        stored_identity.store(&self.context.db())?;
1208        self.identity().set_ready();
1209        Ok(())
1210    }
1211
1212    /// Wait until the serving database exposes the registration identity-topic head.
1213    /// A missing or older head is polled. The timeout also bounds each request.
1214    pub async fn wait_for_registration_visible(
1215        &self,
1216        options: VisibilityConfirmationOptions,
1217    ) -> Result<(), ClientError> {
1218        use xmtp_common::time::{Duration, sleep, timeout};
1219        if !self.identity().is_ready() {
1220            return Err(ClientError::RegistrationNotVisible);
1221        }
1222        let stored: Option<StoredIdentity> = self.context.db().fetch(&())?;
1223        let sequence_id = stored
1224            .and_then(|identity| identity.registration_cursor_sequence_id)
1225            .and_then(|sequence_id| u64::try_from(sequence_id).ok())
1226            .filter(|sequence_id| *sequence_id != 0)
1227            .ok_or(ClientError::RegistrationNotVisible)?;
1228        timeout(Duration::from_millis(options.timeout_ms), async {
1229            let mut delay = REGISTRATION_INITIAL_BACKOFF;
1230            let inbox = hex::decode(self.inbox_id())
1231                .map_err(|_| xmtp_api::ApiError::InvalidRequest("registration inbox id"))?;
1232            let topic = xmtp_proto::types::Topic::new_identity_update(inbox);
1233            xmtp_proto::types::Topic::parse(&topic)?;
1234            loop {
1235                let heads = self
1236                    .context
1237                    .api()
1238                    .newest_topic_cursors(vec![topic.clone()])
1239                    .await?;
1240                let head = heads
1241                    .get(&topic)
1242                    .ok_or(xmtp_api::ApiError::InvalidResponse(
1243                        "registration identity head",
1244                    ))?;
1245                if head.0 >= sequence_id {
1246                    return Ok(());
1247                }
1248                sleep(delay).await;
1249                delay = (delay * 2).min(REGISTRATION_MAX_BACKOFF);
1250            }
1251        })
1252        .await
1253        .map_err(|_| ClientError::RegistrationNotVisible)?
1254    }
1255
1256    /// If no key rotation is scheduled, queue it to occur in the next 5 seconds.
1257    pub fn queue_key_rotation(&self) -> Result<(), ClientError> {
1258        crate::worker::key_package_maintenance::queue_key_rotation(&self.context)?;
1259        Ok(())
1260    }
1261
1262    /// Upload a new key package to the network replacing an existing key package
1263    /// This is expected to be run any time the client receives new Welcome messages
1264    pub async fn rotate_and_upload_key_package(&self) -> Result<(), ClientError> {
1265        // CFG-051 and CFG-061: a latched client publishes nothing. This one
1266        // reaches `Identity` directly instead of going through a group or the
1267        // publish path, so it carries its own gate.
1268        self.ensure_identity_ready()?;
1269        self.identity()
1270            .rotate_and_upload_key_package(
1271                self.context.api(),
1272                self.context.mls_storage(),
1273                CREATE_PQ_KEY_PACKAGE_EXTENSION,
1274            )
1275            .await?;
1276        // The rotation marked superseded KPs delete_at=now+grace; without this
1277        // the parked KpDeletion task would sweep them up to ~30d late.
1278        crate::worker::key_package_maintenance::nudge_deletion(&self.context)?;
1279
1280        Ok(())
1281    }
1282
1283    /// Fetches the current key package from the network for each of the `installation_id`s specified
1284    #[tracing::instrument(skip_all)]
1285    pub async fn get_key_packages_for_installation_ids(
1286        &self,
1287        installation_ids: Vec<Vec<u8>>,
1288    ) -> Result<
1289        HashMap<Vec<u8>, Result<VerifiedKeyPackageV2, KeyPackageVerificationError>>,
1290        ClientError,
1291    > {
1292        MlsStore::new(self.context.clone())
1293            .get_key_packages_for_installation_ids(installation_ids)
1294            .await
1295            .map_err(Into::into)
1296    }
1297
1298    /// Download all unread welcome messages and converts to a group struct, ignoring malformed messages.
1299    /// Returns any new groups created in the operation
1300    #[tracing::instrument(skip_all)]
1301    pub async fn sync_welcomes(&self) -> Result<Vec<MlsGroup<Context>>, GroupError> {
1302        self.ensure_identity_ready()?;
1303        WelcomeService::new(self.context.clone())
1304            .sync_welcomes()
1305            .await
1306    }
1307
1308    /// Sync all groups for the current installation and return the number of groups that were synced.
1309    /// Only active groups will be synced.
1310    #[tracing::instrument(err, skip_all, fields(operation = "sync_all_groups"))]
1311    pub async fn sync_all_groups(
1312        &self,
1313        groups: Vec<MlsGroup<Context>>,
1314    ) -> Result<GroupSyncSummary, GroupError> {
1315        self.ensure_identity_ready()?;
1316        WelcomeService::new(self.context.clone())
1317            .sync_all_groups(groups)
1318            .await
1319    }
1320
1321    /// Sync all unread welcome messages and then sync all groups.
1322    /// Returns the total number of active groups synced.
1323    #[xmtp_common::mls_span]
1324    pub async fn sync_all_welcomes_and_groups(
1325        &self,
1326        consent_states: Option<Vec<ConsentState>>,
1327    ) -> Result<GroupSyncSummary, GroupError> {
1328        self.ensure_identity_ready()?;
1329        WelcomeService::new(self.context.clone())
1330            .sync_all_welcomes_and_groups(consent_states)
1331            .await
1332    }
1333
1334    /// Sweep every group flagged `paused_for_version` and clear the
1335    /// pause flag for any whose floor is now satisfied by this
1336    /// client's `pkg_version`. Pure local-state operation — no
1337    /// network calls. Returns the count of groups unstuck.
1338    ///
1339    /// `sync_all_welcomes_and_groups` already runs this sweep as a
1340    /// preamble; the standalone entry point is for SDKs that want a
1341    /// cheap "post-upgrade recovery" hook independent of the normal
1342    /// sync flow.
1343    pub async fn unstick_paused_groups(&self) -> Result<usize, GroupError> {
1344        self.ensure_identity_ready()?;
1345        WelcomeService::new(self.context.clone())
1346            .unstick_paused_groups()
1347            .await
1348    }
1349
1350    pub async fn sync_all_welcomes_and_device_sync_groups(
1351        &self,
1352    ) -> Result<GroupSyncSummary, ClientError> {
1353        self.sync_welcomes().await?;
1354        self.sync_all_device_sync_groups().await
1355    }
1356
1357    pub async fn sync_all_device_sync_groups(&self) -> Result<GroupSyncSummary, ClientError> {
1358        let groups = self
1359            .context
1360            .db()
1361            .all_sync_groups()?
1362            .into_iter()
1363            .map(|g| {
1364                MlsGroup::new(
1365                    self.context.clone(),
1366                    g.id,
1367                    g.dm_id,
1368                    g.conversation_type,
1369                    g.created_at_ns,
1370                )
1371            })
1372            .collect();
1373
1374        Ok(self.sync_all_groups(groups).await?)
1375    }
1376
1377    /**
1378     * Validates a credential against the given installation public key
1379     *
1380     * This will go to the network and get the latest association state for the inbox.
1381     * It ensures that the installation_pub_key is in that association state
1382     */
1383    pub async fn validate_credential_against_network(
1384        &self,
1385        conn: &DbConnection<<Context::Db as XmtpDb>::Connection>,
1386        credential: &[u8],
1387        installation_pub_key: Vec<u8>,
1388    ) -> Result<InboxId, ClientError> {
1389        let inbox_id = parse_credential(credential)?;
1390        let association_state = IdentityUpdates::new(&self.context)
1391            .get_latest_association_state(conn, &inbox_id)
1392            .await?;
1393        let ident = MemberIdentifier::installation(installation_pub_key);
1394
1395        match association_state.get(&ident) {
1396            Some(_) => Ok(inbox_id),
1397            None => Err(IdentityError::InstallationIdNotFound(inbox_id).into()),
1398        }
1399    }
1400
1401    /// Check whether an account_identifier has a key package registered on the network
1402    ///
1403    /// Arguments:
1404    /// - account_identifier: a list of account identifiers to check
1405    ///
1406    /// Returns:
1407    /// A Vec of booleans indicating whether each account address has a key package registered on the network
1408    pub async fn can_message(
1409        &self,
1410        account_identifiers: &[Identifier],
1411    ) -> Result<HashMap<Identifier, bool>, ClientError> {
1412        // CFG-051 and CFG-061: a latched client issues no request. The latch
1413        // alone, not `ensure_identity_ready`, because this answers before the
1414        // caller has registered an identity.
1415        self.context.server_configuration().check()?;
1416        let requests = account_identifiers.iter().map(Into::into).collect();
1417
1418        let results = self.context.api().get_inbox_ids(requests).await?;
1419        Ok(account_identifiers
1420            .iter()
1421            .cloned()
1422            .zip(results.into_iter().map(|inbox_id| inbox_id.is_some()))
1423            .collect())
1424    }
1425}
1426
1427#[cfg(test)]
1428pub(crate) mod tests;