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