Skip to main content

xmtp_mls/
client.rs

1use crate::{
2    attachments::Attachments,
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    inbox_handles::{self, HandleError, IdentityTarget, account_targets, resolve_verified_in},
14    mls_store::{MlsStore, MlsStoreError},
15    server_configuration::{BlockedConnection, ServerConfigError},
16    subscriptions::{
17        LocalEventError,
18        internal::{emit_consent_changes, emit_deleted_messages},
19    },
20    utils::VersionInfo,
21    worker::device_sync::{DeviceSyncClient, worker::SyncMetric},
22    worker::{WorkerRunner, metrics::WorkerMetrics},
23};
24pub mod notifications;
25pub use crate::core::config::DeviceSync;
26use crate::subscriptions::catch_up::{CatchUpError, CatchUpSummary};
27#[cfg(not(target_arch = "wasm32"))]
28use crate::subscriptions::router_callbacks::{self, ApiClientIdentity};
29#[cfg(any(test, feature = "test-utils"))]
30use crate::subscriptions::stream_messages::stream_stats::StreamWithStats;
31use crate::{
32    core::events::PreferenceUpdate,
33    subscriptions::{self, SubscribeError},
34};
35use crate::{
36    groups::welcome_sync::GroupSyncSummary,
37    identity_updates::{batch_get_association_state_with_verifier, get_creation_signature_kind},
38    messages::{
39        decoded_message::DecodedMessage,
40        enrichment::{EnrichMessageError, enrich_messages},
41    },
42};
43use futures::Stream;
44use notifications::{NotificationConfig, NotificationError, NotificationState};
45use openmls::prelude::tls_codec::Error as TlsCodecError;
46use std::{
47    collections::HashMap,
48    sync::{Arc, atomic::Ordering},
49};
50use thiserror::Error;
51use xmtp_api::{ApiClientWrapper, XmtpApi};
52use xmtp_common::{ErrorCode, Event, Retry, retry_async, retryable};
53use xmtp_common::{MaybeSend, StreamHandle};
54use xmtp_configuration::{
55    CONVERSATION_LIST_DEFAULT_LIMIT, CREATE_PQ_KEY_PACKAGE_EXTENSION,
56    KEY_PACKAGE_ROTATION_INTERVAL_NS, LOCAL_PAGE_MAX_LIMIT,
57};
58use xmtp_cryptography::signature::IdentifierValidationError;
59use xmtp_db::TransactionOutcome::Continue;
60use xmtp_db::consent_record::StoredConsentRecord;
61use xmtp_db::{
62    ConnectionExt, NotFound, StorageError, TransactionOutcome, XmtpDb,
63    consent_record::{
64        ConsentActError, ConsentKey, ConsentState, ConsentType, ConsentWrite, Decision, Owed,
65        conversation_consent, write_consent,
66    },
67    db_connection::DbConnection,
68    encrypted_store::conversation_list::ConversationListItem as DbConversationListItem,
69    group::{ConversationType, GroupMembershipState, GroupQueryArgs},
70    group_message::StoredGroupMessage,
71    identity::StoredIdentity,
72};
73use xmtp_db::{group::GroupQueryOrderBy, prelude::*};
74use xmtp_events::DeletionCause;
75use xmtp_id::associations::handle::Handle;
76use xmtp_id::key_package::{KeyPackageVerificationError, VerifiedKeyPackageV2};
77use xmtp_id::{
78    AsIdRef, InboxId, InboxIdRef, InboxOwner,
79    associations::{
80        AssociationError, AssociationState, Identifier, MemberIdentifier, SignatureError,
81        builder::{SignatureRequest, SignatureRequestError},
82    },
83    scw_verifier::SmartContractSignatureVerifier,
84};
85use xmtp_macro::log_event;
86use xmtp_mls_group_state::{
87    group::{DMMetadataOptions, GroupMetadataOptions},
88    group_metadata::DmMembers,
89    group_mutable_metadata::MessageDisappearingSettings,
90};
91#[cfg(not(target_arch = "wasm32"))]
92use xmtp_proto::api_client::XmtpMlsBidiStreams;
93use xmtp_proto::api_client::XmtpMlsStreams;
94use xmtp_proto::types::InstallationId;
95use xmtp_proto::{
96    api::HasStats,
97    api_client::{ApiStats, IdentityStats, XmtpBackendClient},
98};
99
100use xmtp_proto::types::GroupId;
101/// Enum representing the network the Client is connected to
102#[derive(Clone, Copy, Default, Debug)]
103pub enum Network {
104    Local(&'static str),
105    #[default]
106    Dev,
107    Prod,
108}
109
110/// Timeout for waiting until a registration publish can be read.
111#[derive(Debug, Clone)]
112pub struct VisibilityConfirmationOptions {
113    pub timeout_ms: u64,
114}
115
116const REGISTRATION_INITIAL_BACKOFF: std::time::Duration = std::time::Duration::from_millis(50);
117const REGISTRATION_MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(1);
118
119impl Default for VisibilityConfirmationOptions {
120    fn default() -> Self {
121        Self { timeout_ms: 30_000 }
122    }
123}
124
125/// An app's consent decision about one entity. It carries no
126/// time: the client times it.
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct ConsentAct {
129    pub entity_type: ConsentType,
130    pub entity: String,
131    pub state: ConsentState,
132}
133
134impl ConsentAct {
135    pub fn new(entity_type: ConsentType, state: ConsentState, entity: String) -> Self {
136        Self {
137            entity_type,
138            entity,
139            state,
140        }
141    }
142}
143
144#[derive(Debug, Error, ErrorCode)]
145pub enum ClientError {
146    #[error(transparent)]
147    #[error_code(inherit)]
148    AddressValidation(#[from] IdentifierValidationError),
149    /// Could not publish.
150    ///
151    /// Failed to publish messages to the network. May be retryable.
152    #[error("could not publish: {0}")]
153    PublishError(String),
154    /// Storage error.
155    ///
156    /// Database operation failed. May be retryable.
157    #[error("storage error: {0}")]
158    Storage(#[from] StorageError),
159    /// API error.
160    ///
161    /// Network request to XMTP backend failed. Retryable.
162    #[error("API error: {0}")]
163    Api(#[from] xmtp_api::ApiError),
164    /// Identity error.
165    ///
166    /// Problem with identity operations. Not retryable.
167    #[error("identity error: {0}")]
168    Identity(#[from] crate::identity::IdentityError),
169    /// TLS Codec error.
170    ///
171    /// Encoding/decoding MLS TLS structures failed. Not retryable.
172    #[error("TLS Codec error: {0}")]
173    TlsError(#[from] TlsCodecError),
174    /// Key package verification failed.
175    ///
176    /// Invalid key package received from network. Not retryable.
177    #[error("key package verification: {0}")]
178    KeyPackageVerification(#[from] KeyPackageVerificationError),
179    /// Stream inconsistency.
180    ///
181    /// Message stream state became inconsistent. Not retryable.
182    #[error("Stream inconsistency error: {0}")]
183    StreamInconsistency(String),
184    /// Association error.
185    ///
186    /// Identity association operation failed. Not retryable.
187    #[error("Association error: {0}")]
188    Association(#[from] AssociationError),
189    /// Signature validation error.
190    ///
191    /// A signature failed verification. Not retryable.
192    #[error("signature validation error: {0}")]
193    SignatureValidation(#[from] SignatureError),
194    /// Identity update error.
195    ///
196    /// Failed to process identity update. Not retryable.
197    #[error(transparent)]
198    IdentityUpdate(#[from] IdentityUpdateError),
199    /// Signature request error.
200    ///
201    /// Failed to create/process signature request. Not retryable.
202    #[error(transparent)]
203    SignatureRequest(#[from] SignatureRequestError),
204    /// Group error.
205    ///
206    /// Group operation failed. May be retryable.
207    // the box is to prevent infinite cycle between client and group errors
208    #[error(transparent)]
209    Group(Box<GroupError>),
210    /// Local event error.
211    ///
212    /// Failed to process local event. Not retryable.
213    #[error(transparent)]
214    LocalEvent(#[from] LocalEventError),
215    /// Database connection error.
216    ///
217    /// Connection to database failed. Retryable.
218    #[error(transparent)]
219    Db(#[from] xmtp_db::ConnectionError),
220    /// Message decode failed.
221    ///
222    /// Enrichment returned no decoded message for a stored message. Not
223    /// retryable.
224    #[error("Failed to decode message")]
225    MessageDecodeFailed,
226    /// MLS store error.
227    ///
228    /// OpenMLS key store operation failed. Not retryable.
229    #[error(transparent)]
230    MlsStore(#[from] MlsStoreError),
231    /// Message enrichment error.
232    ///
233    /// Failed to enrich message content. Not retryable.
234    #[error(transparent)]
235    EnrichMessage(#[from] EnrichMessageError),
236    /// Conversion Error
237    ///
238    /// Data type failed to convert. Not retryable.
239    #[error(transparent)]
240    Conversion(#[from] xmtp_proto::ConversionError),
241    #[error(transparent)]
242    #[error_code(inherit)]
243    Handle(#[from] crate::inbox_handles::HandleError),
244    /// Registration not visible.
245    ///
246    /// The registration receipt was invalid or was not visible before the timeout.
247    /// Call `register_identity` again to retry a pending registration.
248    #[error("Registration is not visible")]
249    RegistrationNotVisible,
250    /// Client is closed.
251    ///
252    /// Operation was attempted on a client that has been shut down via
253    /// `Client::close`. Not retryable — build a new client instead.
254    #[error("client is closed")]
255    AlreadyClosed,
256    /// Server configuration unavailable.
257    ///
258    /// The backend did not serve its configuration, or the answer could not be
259    /// stored. A backend older than spec 006 answers `UNIMPLEMENTED`; there is
260    /// no compatibility shim. Retryable exactly when the wrapped failure is:
261    /// an unreachable backend is worth another attempt, `UNIMPLEMENTED` is not.
262    #[error("server configuration unavailable: {0}")]
263    ConfigurationUnavailable(#[source] Box<crate::server_configuration::ConfigurationFetchError>),
264    /// Server configuration invalid.
265    ///
266    /// The backend published a configuration this client cannot use: a missing
267    /// or malformed identifier, a minimum version that does not parse, or a
268    /// chain that is not a CAIP-2 identifier. Not retryable.
269    #[error("server configuration invalid: {0}")]
270    ConfigurationInvalid(#[from] xmtp_configuration::ServerConfigurationError),
271    /// Backend mismatch.
272    ///
273    /// This database is bound to one backend deployment and a different one
274    /// answered. Not retryable — use a database created for the backend this
275    /// app now points at.
276    #[error("this database is bound to backend {stored}, but {received} answered")]
277    BackendMismatch { stored: String, received: String },
278    /// Client version too old.
279    ///
280    /// The backend requires a newer libxmtp than this build. Not retryable —
281    /// ship an updated client.
282    #[error("client version {client} is below the {minimum} this backend requires")]
283    ClientVersionTooOld { client: String, minimum: String },
284    /// Authentication required.
285    ///
286    /// The backend requires a credential and none was configured. Not
287    /// retryable — supply an auth callback or handle before building.
288    #[error(
289        "this backend requires authentication; required scopes: [{}]",
290        required_scopes.join(", ")
291    )]
292    AuthRequired { required_scopes: Vec<String> },
293    /// Chain not accepted.
294    ///
295    /// The backend does not verify smart contract wallet signatures on this
296    /// chain. Not retryable — use a chain the deployment accepts.
297    #[error(
298        "this backend does not accept smart contract wallet signatures on {chain}; accepted: [{}]",
299        accepted.join(", ")
300    )]
301    ChainNotAccepted {
302        chain: String,
303        accepted: Vec<String>,
304    },
305    /// Invalid list options.
306    ///
307    /// A conversation listing named a limit below 1 or above the maximum page
308    /// size, a before conversation id without the before time of its list
309    /// order, or sync or one-shot groups. Not retryable — fix the options.
310    #[error("invalid list options: {0}")]
311    InvalidListOptions(&'static str),
312    /// Consent act refused.
313    ///
314    /// The consent entity cannot be stored, or names a conversation the
315    /// client does not hold. Not retryable.
316    #[error("consent act: {0}")]
317    #[error_code(inherit)]
318    ConsentAct(#[from] ConsentActError),
319}
320
321impl ClientError {
322    #[deny(clippy::wildcard_enum_match_arm)]
323    pub fn db_needs_connection(&self) -> bool {
324        use crate::worker::NeedsDbReconnect;
325        match self {
326            Self::Storage(s) | Self::ConsentAct(ConsentActError::Storage(s)) => {
327                s.db_needs_connection()
328            }
329            Self::Db(c) => c.db_needs_connection(),
330            Self::Group(error) => error.needs_db_reconnect(),
331            Self::Identity(error) => error.needs_db_reconnect(),
332            Self::IdentityUpdate(error) => error.needs_db_reconnect(),
333            Self::MlsStore(error) => error.needs_db_reconnect(),
334            Self::ConfigurationUnavailable(error) => error.db_needs_connection(),
335            Self::ConsentAct(_)
336            | Self::AddressValidation(..)
337            | Self::PublishError(..)
338            | Self::Api(..)
339            | Self::TlsError(..)
340            | Self::KeyPackageVerification(..)
341            | Self::StreamInconsistency(..)
342            | Self::Association(..)
343            | Self::SignatureValidation(..)
344            | Self::SignatureRequest(..)
345            | Self::LocalEvent(..)
346            | Self::MessageDecodeFailed
347            | Self::EnrichMessage(..)
348            | Self::Conversion(..)
349            | Self::Handle(..)
350            | Self::RegistrationNotVisible
351            | Self::AlreadyClosed
352            | Self::ConfigurationInvalid(..)
353            | Self::BackendMismatch { .. }
354            | Self::ClientVersionTooOld { .. }
355            | Self::AuthRequired { .. }
356            | Self::ChainNotAccepted { .. }
357            | Self::InvalidListOptions(..) => false,
358        }
359    }
360}
361
362impl From<NotFound> for ClientError {
363    fn from(value: NotFound) -> Self {
364        ClientError::Storage(StorageError::NotFound(value))
365    }
366}
367
368impl From<GroupError> for ClientError {
369    fn from(err: GroupError) -> ClientError {
370        ClientError::Group(Box::new(err))
371    }
372}
373
374impl From<BlockedConnection> for ClientError {
375    fn from(blocked_connection: BlockedConnection) -> Self {
376        match blocked_connection {
377            BlockedConnection::BackendMismatch { stored, received } => {
378                ClientError::BackendMismatch { stored, received }
379            }
380            BlockedConnection::ClientVersionTooOld { client, minimum } => {
381                ClientError::ClientVersionTooOld { client, minimum }
382            }
383        }
384    }
385}
386
387impl From<ServerConfigError> for ClientError {
388    fn from(error: ServerConfigError) -> Self {
389        match error {
390            ServerConfigError::Unavailable(error) => ClientError::ConfigurationUnavailable(error),
391            ServerConfigError::Invalid(error) => ClientError::ConfigurationInvalid(error),
392            ServerConfigError::Blocked(blocked_connection) => blocked_connection.into(),
393            ServerConfigError::Closed => ClientError::AlreadyClosed,
394        }
395    }
396}
397
398impl xmtp_common::RetryableError for ClientError {
399    fn is_retryable(&self) -> bool {
400        match self {
401            ClientError::Group(group_error) => retryable!(group_error),
402            ClientError::IdentityUpdate(error) => retryable!(error),
403            ClientError::Api(api_error) => retryable!(api_error),
404            ClientError::Storage(storage_error) => retryable!(storage_error),
405            ClientError::Db(db) => retryable!(db),
406            // SCW verification errors carry retryability through SignatureError;
407            // transient RPC provider failures must not advance the welcome cursor.
408            // See xmtp/libxmtp#3394.
409            ClientError::SignatureValidation(e) => retryable!(e),
410            // A backend that was briefly unreachable, or a database that was
411            // briefly locked, is worth another attempt: the refresh worker's
412            // backoff depends on this answer.
413            ClientError::ConfigurationUnavailable(e) => retryable!(e),
414            _ => false,
415        }
416    }
417
418    fn rate_limit_delay(&self) -> Option<xmtp_common::time::Duration> {
419        match self {
420            ClientError::Group(group_error) => group_error.rate_limit_delay(),
421            ClientError::IdentityUpdate(error) => error.rate_limit_delay(),
422            ClientError::Api(api_error) => api_error.rate_limit_delay(),
423            ClientError::SignatureValidation(e) => e.rate_limit_delay(),
424            ClientError::ConfigurationUnavailable(e) => e.rate_limit_delay(),
425            _ => None,
426        }
427    }
428}
429
430/// The structured stream failure in `error`, read as
431/// [`stream_failure_details`](crate::subscriptions::stream_failure::stream_failure_details)
432/// does, and also through [`ClientError::Group`], whose `source()` skips the
433/// group error because the variant is transparent.
434pub fn stream_failure_details(
435    error: &(dyn std::error::Error + 'static),
436) -> Option<crate::subscriptions::stream_failure::StreamFailureDetails> {
437    crate::subscriptions::stream_failure::stream_failure_details_opening(error, |error| {
438        match error.downcast_ref::<ClientError>() {
439            Some(ClientError::Group(error)) => Some(error.as_ref()),
440            _ => None,
441        }
442    })
443}
444
445/// Clients manage access to the network, identity, and data store
446pub struct Client<Context> {
447    pub context: Context,
448    pub installation_id: InstallationId,
449    pub(crate) workers: Arc<WorkerRunner>,
450    pub(crate) app_lifetime: Arc<AppLifetime>,
451}
452
453pub(crate) struct AppLifetime {
454    pub(crate) events: xmtp_events::EventBus<crate::subscriptions::internal::InternalEvent>,
455}
456
457impl Drop for AppLifetime {
458    fn drop(&mut self) {
459        self.events.close_app_subscriptions();
460    }
461}
462
463impl<Context> Drop for Client<Context> {
464    fn drop(&mut self) {
465        log_event!(Event::ClientDropped, self.installation_id);
466    }
467}
468
469// most of these things are `Arc`'s
470impl<Context: Clone> Clone for Client<Context> {
471    fn clone(&self) -> Self {
472        Self {
473            context: self.context.clone(),
474            installation_id: self.installation_id,
475            workers: self.workers.clone(),
476            app_lifetime: self.app_lifetime.clone(),
477        }
478    }
479}
480
481impl<Context> Client<Context>
482where
483    Context: XmtpSharedContext,
484{
485    pub fn identity_updates(&self) -> IdentityUpdates<&Context> {
486        IdentityUpdates::new(&self.context)
487    }
488
489    pub fn mls_store(&self) -> MlsStore<Context> {
490        MlsStore::new(self.context.clone())
491    }
492
493    pub fn scw_verifier(&self) -> Arc<Box<dyn SmartContractSignatureVerifier>> {
494        self.context.scw_verifier()
495    }
496
497    pub fn version_info(&self) -> &VersionInfo {
498        self.context.version_info()
499    }
500}
501
502impl<Context> Client<Context>
503where
504    Context: XmtpSharedContext,
505    Context::ApiClient: HasStats,
506{
507    pub fn api_stats(&self) -> ApiStats {
508        self.context.api().api_client.mls_stats()
509    }
510
511    pub fn identity_api_stats(&self) -> IdentityStats {
512        self.context.api().api_client.identity_stats()
513    }
514
515    pub fn clear_stats(&self) {
516        self.context.api().api_client.mls_stats().clear();
517        self.context.api().api_client.identity_stats().clear();
518    }
519}
520
521/// Get the [`AssociationState`] for each `inbox_id`
522pub async fn inbox_addresses_with_verifier<ApiClient: XmtpApi>(
523    api_client: &ApiClientWrapper<ApiClient>,
524    conn: &impl DbQuery,
525    inbox_ids: Vec<InboxIdRef<'_>>,
526    scw_verifier: &impl SmartContractSignatureVerifier,
527) -> Result<Vec<AssociationState>, ClientError> {
528    load_identity_updates(api_client, conn, &inbox_ids).await?;
529    let state = batch_get_association_state_with_verifier(
530        conn,
531        &inbox_ids.into_iter().map(|i| (i, None)).collect::<Vec<_>>(),
532        scw_verifier,
533    )
534    .await?;
535    Ok(state)
536}
537
538impl<Context> Client<Context>
539where
540    Context: XmtpSharedContext + 'static,
541{
542    /// Reconnect to the client's database if it has previously been released
543    pub fn reconnect_db(&self) -> Result<(), ClientError> {
544        // Hold the call gate until the workers start. `close` waits for this
545        // call before it stops the workers, so it also stops the ones started
546        // here.
547        let _call = self
548            .context
549            .foreground_calls()
550            .enter()
551            .ok_or(ClientError::AlreadyClosed)?;
552        if self.context.is_closed() {
553            return Err(ClientError::AlreadyClosed);
554        }
555        #[cfg(all(test, not(target_arch = "wasm32")))]
556        test_hooks::pause_reconnect(&self.context.installation_id().to_vec());
557        self.context.db().reconnect().map_err(StorageError::from)?;
558        self.workers.spawn(self.context.clone());
559        Ok(())
560    }
561
562    /// Cleanly shut down this client: cancel in-flight workers and streams,
563    /// then release the DB connection. Idempotent — a second call is `Ok(())`.
564    ///
565    /// Callers (notably the Node binding consumers) should `await` this before
566    /// deleting the SQLite file or dropping the client wrapper, to avoid late
567    /// log spew from detached workers/streams firing against a dead DB.
568    pub async fn close(&self) -> Result<(), ClientError> {
569        // `shutdown_complete` is distinct from `is_closed()` (which reflects
570        // cancellation): only set after the DB actually disconnects. If
571        // `disconnect()` errors below, callers can retry `close()` and we'll
572        // attempt disconnect again rather than silently short-circuiting.
573        if self.context.shutdown_complete() {
574            return Ok(());
575        }
576        self.context
577            .api()
578            .api_client
579            .unregister_client_event_writer(self.context.public_event_writer());
580        self.context.events().close_app_subscriptions();
581        self.context.cancellation_token().cancel();
582        // A running host call can hold a pooled connection, and a running
583        // reconnect can start workers. Wait for both before the workers stop
584        // and the database disconnects.
585        self.context.foreground_calls().close_and_wait().await;
586        let delivery_result = self.context.close_message_delivery();
587        self.workers.shutdown().await;
588        self.context.events().close_internal_subscriptions();
589        // Keep the database connected when lease release fails so a later
590        // close call can retry it.
591        delivery_result?;
592        self.context
593            .db()
594            .disconnect()
595            .map_err(xmtp_db::StorageError::from)?;
596        self.context.mark_shutdown_complete();
597        log_event!(Event::ClientClosed, self.installation_id);
598        Ok(())
599    }
600
601    /// yields until the sync worker notifies that it is initialized and running.
602    pub async fn wait_for_sync_worker_init(&self) {
603        self.workers.wait_for_sync_worker_init().await;
604    }
605
606    pub fn sync_metrics(&self) -> Option<Arc<WorkerMetrics<SyncMetric>>> {
607        self.workers.sync_metrics()
608    }
609}
610
611impl<Context> Client<Context>
612where
613    Context: XmtpSharedContext,
614{
615    /// What this deployment published about itself, as resolved at build.
616    /// A refresh rewrites the stored copy; it never changes
617    /// this value.
618    pub fn server_configuration(&self) -> &xmtp_configuration::ServerConfiguration {
619        self.context.server_configuration().configuration()
620    }
621
622    /// Fetch the deployment configuration now and rewrite the stored copy.
623    ///
624    /// Applies the same validation, storage, and identifier
625    /// binding the refresh worker applies. The snapshot this client is
626    /// holding is unchanged: a new value takes effect at the next build.
627    pub async fn refresh_server_configuration(
628        &self,
629    ) -> Result<xmtp_configuration::ServerConfiguration, ClientError> {
630        Ok(crate::core::preflight::refresh(&self.context).await?)
631    }
632
633    /// Retrieves the client's installation public key, sometimes also called `installation_id`
634    pub fn installation_public_key(&self) -> InstallationId {
635        self.context.installation_id()
636    }
637    /// Retrieves the client's inbox ID
638    pub fn inbox_id(&self) -> InboxIdRef<'_> {
639        self.context.identity().inbox_id()
640    }
641
642    /// get a reference to the monolithic Database object where
643    /// higher-level queries are defined
644    pub fn db(&self) -> <Context::Db as XmtpDb>::DbQuery {
645        self.context.db()
646    }
647
648    /// This associates an installation_id with a human-readable
649    /// name and makes the logs a little easier to read.
650    #[cfg(any(test, feature = "test-utils"))]
651    pub fn set_name(&self, name: &str) {
652        log_event!(Event::AssociateName, self.context.installation_id(), name);
653    }
654
655    pub fn device_sync_client(&self) -> DeviceSyncClient<Context> {
656        let metrics = self.context.sync_metrics();
657        DeviceSyncClient::new(
658            self.context.clone(),
659            metrics.unwrap_or(Arc::new(WorkerMetrics::new(self.context.installation_id()))),
660        )
661    }
662
663    /// The inbox that holds `identifier`, or `None`. The backend's answer
664    /// counts only when the newest state the client derives for the answered
665    /// inbox holds the identifier.
666    pub async fn find_inbox_id_from_identifier(
667        &self,
668        identifier: Identifier,
669    ) -> Result<Option<String>, ClientError> {
670        Ok(
671            resolve_verified_in(&self.context, &[IdentityTarget::Account(identifier)])
672                .await?
673                .pop()
674                .flatten(),
675        )
676    }
677
678    /// Get the highest `sequence_id` from the local database for the client's `inbox_id`.
679    /// This may not be consistent with the latest state on the backend.
680    pub fn inbox_sequence_id(
681        &self,
682        conn: &DbConnection<<Context::Db as XmtpDb>::Connection>,
683    ) -> Result<i64, StorageError> {
684        self.context
685            .identity()
686            .sequence_id(conn)
687            .map_err(Into::into)
688    }
689
690    /// Get the [`AssociationState`] for the client's `inbox_id`
691    pub async fn inbox_state(
692        &self,
693        refresh_from_network: bool,
694    ) -> Result<AssociationState, ClientError> {
695        let conn = self.context.db();
696        let inbox_id = self.inbox_id();
697        if refresh_from_network {
698            load_identity_updates_for_client(&self.context, &conn, &[inbox_id]).await?;
699        }
700        let identity_service = IdentityUpdates::new(&self.context);
701        let state = identity_service
702            .get_association_state(&conn, inbox_id, None)
703            .await?;
704        Ok(state)
705    }
706
707    /// Get the [`AssociationState`] for each `inbox_id`
708    pub async fn inbox_addresses(
709        &self,
710        refresh_from_network: bool,
711        inbox_ids: Vec<InboxIdRef<'_>>,
712    ) -> Result<Vec<AssociationState>, ClientError> {
713        let conn = self.context.db();
714        if refresh_from_network {
715            load_identity_updates_for_client(&self.context, &conn, &inbox_ids).await?;
716        }
717        let identity_service = IdentityUpdates::new(&self.context);
718        let state = identity_service
719            .batch_get_association_state(
720                &conn,
721                &inbox_ids.into_iter().map(|i| (i, None)).collect::<Vec<_>>(),
722            )
723            .await?;
724        Ok(state)
725    }
726
727    /// Get the total number of inbox updates for `inbox_ids`. `refresh_from_network` will force
728    /// a network refresh. May still access network if an inbox_id does not yet exist in the local
729    /// cache.
730    pub async fn fetch_inbox_updates_count(
731        &self,
732        refresh_from_network: bool,
733        inbox_ids: Vec<InboxIdRef<'_>>,
734    ) -> Result<HashMap<InboxId, u32>, ClientError> {
735        let conn = self.context.db();
736        if refresh_from_network {
737            load_identity_updates_for_client(&self.context, &conn, &inbox_ids).await?;
738        }
739        let inbox_id_strs = inbox_ids.to_vec();
740        let counts = conn.count_inbox_updates(&inbox_id_strs)?;
741        Ok(counts.into_iter().map(|(k, v)| (k, v as u32)).collect())
742    }
743
744    /// Get the total number of inbox updates for the client's inbox_id.
745    /// Setting `refresh_from_network` forces a network refresh, otherwise
746    /// this operation is offline.
747    pub async fn fetch_own_inbox_updates_count(
748        &self,
749        refresh_from_network: bool,
750    ) -> Result<u32, ClientError> {
751        let inbox_id = self.inbox_id();
752        Ok(self
753            .fetch_inbox_updates_count(refresh_from_network, vec![inbox_id])
754            .await?
755            .get(inbox_id)
756            .copied()
757            .unwrap_or(0))
758    }
759
760    /// Get the signature kind used to create an inbox.
761    ///
762    /// # Arguments
763    /// * `inbox_id` - The inbox ID to check
764    /// * `refresh_from_network` - Whether to fetch updates from the network first
765    ///
766    /// # Returns
767    /// * `Some(SignatureKind)` - The signature kind used to create the inbox
768    /// * `None` - Inbox doesn't exist or creation info is unavailable
769    pub async fn inbox_creation_signature_kind(
770        &self,
771        inbox_id: InboxIdRef<'_>,
772        refresh_from_network: bool,
773    ) -> Result<Option<xmtp_id::associations::SignatureKind>, ClientError> {
774        let conn = self.context.db();
775
776        // Load the first identity update (creation update) for this inbox if requested
777        if refresh_from_network {
778            load_identity_updates_for_client(&self.context, &conn, &[inbox_id]).await?;
779        }
780
781        let verifier = self.context.scw_verifier();
782
783        let signature_kind = get_creation_signature_kind(&conn, verifier, inbox_id).await?;
784
785        Ok(signature_kind)
786    }
787
788    /// Store the app's consent decisions in one transaction. An
789    /// act on a group id the client does not hold fails the whole call, and
790    /// nothing is stored.
791    pub async fn set_consent_states(&self, acts: &[ConsentAct]) -> Result<(), ClientError> {
792        let own = self.context.inbox_id();
793        let keys = acts
794            .iter()
795            .map(|act| ConsentKey::for_app(&self.context.db(), own, act.entity_type, &act.entity))
796            .collect::<Result<Vec<_>, _>>()?;
797        crate::state_tx::state_write_with_events(
798            self.context.mls_storage(),
799            self.context.events(),
800            |tx, events| {
801                let storage = tx.storage();
802                let db = storage.db();
803                let mut changes = vec![];
804                for (key, act) in keys.iter().zip(acts) {
805                    let decision = ConsentWrite::Decide(Decision::Set(act.state));
806                    changes.extend(write_consent(&db, key, decision, Owed::Mark)?);
807                }
808                emit_consent_changes(&self.context, events, &db, &changes)?;
809                Ok::<_, StorageError>(Continue(()))
810            },
811        )?
812        .into_continued();
813
814        Ok(())
815    }
816
817    /// The consent an app reads for an entity: a conversation's under
818    /// its key, held or not, or an inbox's record.
819    // implements: CONS-001
820    pub async fn get_consent_state(
821        &self,
822        entity_type: ConsentType,
823        entity: String,
824    ) -> Result<ConsentState, ClientError> {
825        let db = self.context.db();
826        let Some(key) = ConsentKey::parse(entity_type, &entity) else {
827            return Ok(ConsentState::Unknown);
828        };
829        Ok(match key.group_id() {
830            Some(id) => conversation_consent(&db, self.context.inbox_id(), &id)?,
831            None => db
832                .consent_row(&key)?
833                .map_or(ConsentState::Unknown, |r| r.state),
834        })
835    }
836
837    /// Release the client's database connection
838    pub fn release_db_connection(&self) -> Result<(), ClientError> {
839        self.context
840            .db()
841            .disconnect()
842            .map_err(xmtp_db::StorageError::from)?;
843        Ok(())
844    }
845
846    /// Get a reference to the client's identity struct
847    pub fn identity(&self) -> &Identity {
848        self.context.identity()
849    }
850
851    /// Ensures identity is ready before performing operations.
852    /// Call `register_identity()` first if this fails.
853    pub(crate) fn ensure_identity_ready<E: From<BlockedConnection> + From<IdentityError>>(
854        &self,
855    ) -> Result<(), E> {
856        // Once the connection is blocked, every later call fails with the
857        // reason. This is the gate every client-level operation already passes
858        // through, so the check costs nothing extra.
859        self.context.server_configuration().check()?;
860        if !self.identity().is_ready() {
861            tracing::warn!(
862                inbox_id = %self.inbox_id(),
863                "Operation attempted before register_identity() was called"
864            );
865            return Err(IdentityError::UninitializedIdentity.into());
866        }
867        Ok(())
868    }
869
870    /// Create a new group with the default settings
871    /// Applies a custom [`PolicySet`] to the group if one is specified
872    pub fn create_group(
873        &self,
874        permissions_policy_set: Option<PolicySet>,
875        opts: Option<GroupMetadataOptions>,
876    ) -> Result<MlsGroup<Context>, ClientError> {
877        self.ensure_identity_ready::<ClientError>()?;
878
879        let group: MlsGroup<Context> = MlsGroup::create_and_insert(
880            self.context.clone(),
881            ConversationType::Group,
882            permissions_policy_set.unwrap_or_default(),
883            opts.unwrap_or_default(),
884            None,
885        )?;
886
887        log_event!(
888            Event::CreatedGroup,
889            self.context.installation_id(),
890            group_id = group.group_id
891        );
892
893        Ok(group)
894    }
895
896    /// Create a group with an initial set of members added. Every account
897    /// resolves before the group is stored, as do member identities and key
898    /// packages. An account with no inbox fails with [`GroupError::AddressNotFound`].
899    pub async fn create_group_with_identifiers(
900        &self,
901        account_identifiers: &[Identifier],
902        permissions_policy_set: Option<PolicySet>,
903        opts: Option<GroupMetadataOptions>,
904    ) -> Result<MlsGroup<Context>, ClientError> {
905        self.create_group_with_targets(
906            &account_targets(account_identifiers),
907            permissions_policy_set,
908            opts,
909        )
910        .await
911    }
912
913    /// Returns the stored group if its first add is still pending when the wait
914    /// ends. A later sync completes the add.
915    #[tracing::instrument(level = "debug", skip_all, fields(size = inbox_ids.len()))]
916    pub async fn create_group_with_members(
917        &self,
918        inbox_ids: &[impl AsIdRef],
919        permissions_policy_set: Option<PolicySet>,
920        opts: Option<GroupMetadataOptions>,
921    ) -> Result<MlsGroup<Context>, ClientError> {
922        tracing::info!("creating group");
923        self.ensure_identity_ready::<ClientError>()?;
924        let opts = opts.unwrap_or_default();
925        let draft = MlsGroup::<Context>::prepare_new(
926            &self.context,
927            None,
928            ConversationType::Group,
929            permissions_policy_set.unwrap_or_default(),
930            &opts,
931            None,
932        )?;
933        let ids = inbox_ids.iter().map(AsIdRef::as_ref).collect::<Vec<_>>();
934        // implements: GMOD-056
935        let add = MlsGroup::<Context>::initial_add(&self.context, &draft, &ids).await?;
936        let (stored, intent) = MlsGroup::<Context>::store_new(
937            &self.context,
938            draft,
939            GroupMembershipState::Allowed,
940            ConversationType::Group,
941            opts,
942            true,
943            add,
944        )?;
945        let group = MlsGroup::new(
946            self.context.clone(),
947            stored.id,
948            stored.dm_id,
949            stored.conversation_type,
950            stored.created_at_ns,
951        );
952        log_event!(
953            Event::CreatedGroup,
954            self.context.installation_id(),
955            group_id = group.group_id
956        );
957        let Some(intent) = intent else {
958            return Ok(group);
959        };
960        match group.sync_until_intent_resolved(intent.id).await {
961            Err(error) if !group.intent_pending(intent.id)? => return Err(error.into()),
962            Err(error) => tracing::warn!(?error, "first add pending; a sync finishes it"),
963            Ok(_) => {}
964        }
965
966        Ok(group)
967    }
968
969    /// Create a new Direct Message with the default settings
970    #[tracing::instrument(level = "debug", skip_all)]
971    async fn create_dm_by_inbox_id(
972        &self,
973        target_inbox_id: InboxId,
974        opts: Option<DMMetadataOptions>,
975    ) -> Result<MlsGroup<Context>, ClientError> {
976        let group: MlsGroup<Context> = MlsGroup::create_dm_and_insert(
977            &self.context,
978            GroupMembershipState::Allowed,
979            target_inbox_id.clone(),
980            GroupMetadataOptions {
981                message_disappearing_settings: opts
982                    .unwrap_or_default()
983                    .message_disappearing_settings,
984                ..Default::default()
985            },
986            None,
987        )?;
988
989        log_event!(
990            Event::CreatedDM,
991            self.context.installation_id(),
992            group_id = group.group_id,
993            target_inbox = target_inbox_id
994        );
995        group.add_members(&[target_inbox_id]).await?;
996
997        Ok(group)
998    }
999
1000    /// Find or create a Direct Message with the default settings
1001    pub async fn find_or_create_dm_by_identity(
1002        &self,
1003        target_identity: Identifier,
1004        opts: Option<DMMetadataOptions>,
1005    ) -> Result<MlsGroup<Context>, ClientError> {
1006        self.ensure_identity_ready::<ClientError>()?;
1007        tracing::info!("finding or creating dm with address: {target_identity}");
1008        let inbox_id = match self
1009            .find_inbox_id_from_identifier(target_identity.clone())
1010            .await?
1011        {
1012            Some(id) => id,
1013            None => {
1014                return Err(NotFound::InboxIdForAddress(target_identity.to_string()).into());
1015            }
1016        };
1017
1018        self.find_or_create_dm(inbox_id, opts).await
1019    }
1020
1021    /// Find or create a Direct Message by inbox_id with the default settings.
1022    /// A stored DM whose creation stopped before the peer was added gets the
1023    /// peer first, and any failure of that add is returned, as a new DM's
1024    /// creation returns its add's failure.
1025    pub async fn find_or_create_dm(
1026        &self,
1027        inbox_id: impl AsIdRef,
1028        opts: Option<DMMetadataOptions>,
1029    ) -> Result<MlsGroup<Context>, ClientError> {
1030        self.ensure_identity_ready::<ClientError>()?;
1031        let inbox_id = inbox_id.as_ref();
1032        tracing::info!("finding or creating dm with inbox_id: {}", inbox_id);
1033        let db = self.context.db();
1034        let group = db.find_active_dm_group(&DmMembers {
1035            member_one_inbox_id: self.inbox_id(),
1036            member_two_inbox_id: inbox_id,
1037        })?;
1038
1039        if let Some(group) = group {
1040            let group = MlsGroup::new(
1041                self.context.clone(),
1042                group.id,
1043                group.dm_id,
1044                group.conversation_type,
1045                group.created_at_ns,
1046            );
1047            // implements: DMS-020
1048            group.add_missing_dm_peer().await?;
1049            return Ok(group);
1050        }
1051        self.create_dm_by_inbox_id(inbox_id.to_string(), opts).await
1052    }
1053
1054    /// Look up a group by its ID
1055    ///
1056    /// Returns a [`MlsGroup`] if the group exists, or an error if it does not
1057    ///
1058    pub fn group(&self, group_id: &GroupId) -> Result<MlsGroup<Context>, ClientError> {
1059        MlsGroup::load_by_id(&self.context, group_id).map_err(Into::into)
1060    }
1061
1062    /// Look up a group by its ID while stitching DMs
1063    ///
1064    /// Returns a [`MlsGroup`] if the group exists, or an error if it does not
1065    ///
1066    pub fn stitched_group(&self, group_id: &GroupId) -> Result<MlsGroup<Context>, ClientError> {
1067        let conn = self.context.db();
1068        let stored_group = conn.fetch_stitched(group_id)?;
1069        stored_group
1070            .map(|g| {
1071                MlsGroup::new(
1072                    self.context.clone(),
1073                    g.id,
1074                    g.dm_id,
1075                    g.conversation_type,
1076                    g.created_at_ns,
1077                )
1078            })
1079            .ok_or(NotFound::GroupById(*group_id))
1080            .map_err(Into::into)
1081    }
1082
1083    /// Find all the duplicate dms for this group
1084    pub fn find_duplicate_dms_for_group(
1085        &self,
1086        group_id: &GroupId,
1087    ) -> Result<Vec<MlsGroup<Context>>, ClientError> {
1088        let (group, _) = MlsGroup::new_cached(self.context.clone(), group_id)?;
1089        Ok(group.find_duplicate_dms()?)
1090    }
1091
1092    /// Fetches the message disappearing settings for a given group ID.
1093    ///
1094    /// Returns `Some(MessageDisappearingSettings)` if the group exists and has valid settings,
1095    /// `None` if the group or settings are missing, or `Err(ClientError)` on a database error.
1096    pub fn group_disappearing_settings(
1097        &self,
1098        group_id: &GroupId,
1099    ) -> Result<Option<MessageDisappearingSettings>, ClientError> {
1100        let (group, _) = MlsGroup::new_cached(self.context.clone(), group_id)?;
1101        Ok(group.disappearing_settings()?)
1102    }
1103
1104    /**
1105     * Look up a DM group by the target's inbox_id.
1106     *
1107     * Returns a [`MlsGroup`] if the group exists, or an error if it does not
1108     */
1109    pub fn dm_group_from_target_inbox(
1110        &self,
1111        target_inbox_id: String,
1112    ) -> Result<MlsGroup<Context>, ClientError> {
1113        let conn = self.context.db();
1114
1115        let group = conn
1116            .find_active_dm_group(&DmMembers {
1117                member_one_inbox_id: self.inbox_id(),
1118                member_two_inbox_id: &target_inbox_id,
1119            })?
1120            .ok_or(NotFound::DmByInbox(target_inbox_id))?;
1121        Ok(MlsGroup::new(
1122            self.context.clone(),
1123            group.id,
1124            group.dm_id,
1125            group.conversation_type,
1126            group.created_at_ns,
1127        ))
1128    }
1129
1130    /// Look up a message by its ID
1131    /// Returns a [`StoredGroupMessage`] if the message exists, or an error if it does not
1132    pub fn message(&self, message_id: Vec<u8>) -> Result<StoredGroupMessage, ClientError> {
1133        let conn = &mut self.context.db();
1134        // implements: META-051
1135        let message =
1136            conn.get_app_visible_group_message(&message_id, xmtp_common::time::now_ns())?;
1137        Ok(message.ok_or(NotFound::MessageById(message_id))?)
1138    }
1139
1140    /// Look up a stored message and its current conversation.
1141    /// A message in a duplicate DM resolves to the stitched conversation.
1142    pub async fn message_with_group(
1143        &self,
1144        id: &[u8],
1145    ) -> Result<Option<(StoredGroupMessage, MlsGroup<Context>)>, ClientError> {
1146        let message = match self.message(id.to_vec()) {
1147            Ok(message) => message,
1148            Err(ClientError::Storage(StorageError::NotFound(NotFound::MessageById(_)))) => {
1149                return Ok(None);
1150            }
1151            Err(error) => return Err(error),
1152        };
1153        let group = self.stitched_group(&message.group_id)?;
1154        Ok(Some((message, group)))
1155    }
1156
1157    /// Look up and enrich a message by its ID, returning a [`DecodedMessage`]
1158    /// Returns an error if the message is not found or if it cannot be decoded/enriched
1159    #[xmtp_common::mls_span]
1160    pub fn message_v2(&self, message_id: Vec<u8>) -> Result<DecodedMessage, ClientError> {
1161        let conn = self.context.db();
1162        let message = conn
1163            .get_app_visible_group_message(&message_id, xmtp_common::time::now_ns())?
1164            .ok_or_else(|| NotFound::MessageById(message_id.clone()))?;
1165
1166        let group_id = message.group_id;
1167
1168        let enriched = enrich_messages(conn, &group_id, vec![message])?;
1169
1170        // Since enrich_messages returns a Vec<DecodedMessage>, we can use .into_iter().next().ok_or(...) to take ownership without cloning.
1171        enriched
1172            .into_iter()
1173            .next()
1174            // In practice `enrich_messages` should always return an array of the same length as the input
1175            .ok_or(ClientError::MessageDecodeFailed)
1176    }
1177
1178    /// Delete a message by its ID
1179    /// This method is idempotent and will not error if the message is not found
1180    /// Returns the number of messages deleted (0 or 1)
1181    pub fn delete_message(&self, message_id: Vec<u8>) -> Result<usize, ClientError> {
1182        Ok(crate::state_tx::state_write_with_events(
1183            self.context.mls_storage(),
1184            self.context.events(),
1185            |tx, events| {
1186                let storage = tx.storage();
1187                let db = storage.db();
1188                let message = db.get_group_message(&message_id)?;
1189                let deleted = db.delete_message_by_id(&message_id)?;
1190                if deleted > 0
1191                    && let Some(message) = message
1192                {
1193                    emit_deleted_messages(
1194                        events,
1195                        vec![message],
1196                        DeletionCause::DeletedLocally,
1197                        // A local removal of the row: no inbox sent a delete.
1198                        None,
1199                        &db,
1200                    )?;
1201                }
1202                Ok::<_, StorageError>(Continue(deleted))
1203            },
1204        )?
1205        .into_continued())
1206    }
1207
1208    /// Query for groups with optional filters
1209    ///
1210    /// Filters:
1211    /// - allowed_states: only return groups with the given membership states
1212    /// - created_after_ns: only return groups created after the given timestamp (in nanoseconds)
1213    /// - created_before_ns: only return groups created before the given timestamp (in nanoseconds)
1214    /// - limit: only return the first `limit` groups
1215    pub fn find_groups(&self, args: GroupQueryArgs) -> Result<Vec<MlsGroup<Context>>, ClientError> {
1216        MlsGroup::find(&self.context, args).map_err(Into::into)
1217    }
1218
1219    /// List one page of the app's conversations, in activity order unless
1220    /// `args` names another order. A page holds at most `args.limit`
1221    /// conversations, or [`CONVERSATION_LIST_DEFAULT_LIMIT`] when it names
1222    /// none. To read the next page, pass the last row's `listing_time_ns` as
1223    /// the order's before time and its group id as `before_id`.
1224    ///
1225    /// Internal callers that need every group use `find_groups` or
1226    /// `fetch_conversation_list`, which apply no default limit.
1227    // implements: CONS-046, CONS-049
1228    pub fn list_conversations(
1229        &self,
1230        mut args: GroupQueryArgs,
1231    ) -> Result<Vec<ConversationListItem<Context>>, ClientError> {
1232        if args.order_by.is_none() {
1233            args.order_by = Some(GroupQueryOrderBy::LastActivity);
1234        }
1235        let limit = args
1236            .limit
1237            .unwrap_or(i64::from(CONVERSATION_LIST_DEFAULT_LIMIT));
1238        if !(1..=i64::from(LOCAL_PAGE_MAX_LIMIT)).contains(&limit) {
1239            return Err(ClientError::InvalidListOptions(
1240                "limit must be at least 1 and at most the maximum page size",
1241            ));
1242        }
1243        args.limit = Some(limit);
1244        if args.before_id.is_some() && args.order_before_ns().is_none() {
1245            return Err(ClientError::InvalidListOptions(
1246                "before_id needs the before time of the list order",
1247            ));
1248        }
1249        // Requested sync groups bypass the page's limit, order and cursor, and
1250        // a sync group is not a conversation.
1251        if args.include_sync_groups || args.conversation_type.is_some_and(|kind| kind.is_virtual())
1252        {
1253            return Err(ClientError::InvalidListOptions(
1254                "a conversation listing cannot include sync or one-shot groups",
1255            ));
1256        }
1257        Ok(self
1258            .context
1259            .db()
1260            .fetch_conversation_list(args)?
1261            .into_iter()
1262            .map(
1263                |conversation_item: DbConversationListItem| ConversationListItem {
1264                    membership_state: conversation_item.membership_state,
1265                    added_by_inbox_id: conversation_item.added_by_inbox_id,
1266                    group: MlsGroup::new(
1267                        self.context.clone(),
1268                        conversation_item.id,
1269                        conversation_item.dm_id,
1270                        conversation_item.conversation_type,
1271                        conversation_item.created_at_ns,
1272                    ),
1273                    is_commit_log_forked: conversation_item.is_commit_log_forked,
1274                    listing_time_ns: conversation_item.listing_time_ns,
1275                },
1276            )
1277            .collect())
1278    }
1279
1280    /// Register this installation with an inbox owner and wait until it is visible.
1281    pub async fn register_with_owner(&self, owner: &impl InboxOwner) -> Result<(), ClientError> {
1282        if self.identity().is_ready() {
1283            return self.ensure_registration_visible().await;
1284        }
1285
1286        let mut signature_request = self
1287            .context
1288            .signature_request()
1289            .ok_or(SignatureRequestError::MissingSigner)?;
1290        let signature = owner
1291            .sign(&signature_request.signature_text())
1292            .map_err(SignatureError::from)?;
1293        signature_request
1294            .add_signature(signature, self.scw_verifier())
1295            .await?;
1296        self.register_identity(signature_request).await
1297    }
1298
1299    /// Upload the key package before the identity update exposes this installation.
1300    /// Record its receipt for key retirement and retain the registration cursor.
1301    /// Return success only after the backend can read the registration receipt.
1302    #[xmtp_common::mls_span]
1303    pub async fn register_identity(
1304        &self,
1305        signature_request: SignatureRequest,
1306    ) -> Result<(), ClientError> {
1307        tracing::info!("registering identity");
1308        // Registration is a network call like any other.
1309        self.context.server_configuration().check()?;
1310
1311        // Resume visibility confirmation after a stored registration.
1312        let stored_identity: Option<StoredIdentity> = self.context.db().fetch(&())?;
1313        if stored_identity.is_some() {
1314            tracing::info!("Resuming stored identity registration");
1315            self.identity().set_ready();
1316            return self.ensure_registration_visible().await;
1317        }
1318
1319        // Step 1: Generate key package and store locally (not uploaded yet)
1320        let (kp_bytes, history_id) = crate::identity_lifecycle::generate_and_store_key_package(
1321            self.identity(),
1322            self.context.mls_storage(),
1323            CREATE_PQ_KEY_PACKAGE_EXTENSION,
1324        )?;
1325
1326        // Step 2: Validate signatures (fails here if invalid - no network pollution)
1327        let identity_update = signature_request
1328            .build_identity_update()
1329            .map_err(IdentityUpdateError::from)?;
1330        identity_update
1331            .to_verified(&self.context.scw_verifier())
1332            .await?;
1333
1334        // Step 3: Upload key package first (prevents race condition)
1335        let key_package_meta = self.context.api().upload_key_package(kp_bytes).await?;
1336        let key_package_cursor = key_package_meta
1337            .cursor
1338            .filter(|cursor| cursor.sequence_id > 0)
1339            .ok_or(xmtp_api::ApiError::InvalidResponse(
1340                "key package publish cursor",
1341            ))?;
1342
1343        // Own-installation changes are compared from before publication. A
1344        // conflict retry can load another installation's update into this
1345        // database before the final reload, and those changes are reported too.
1346        let inbox_id = self.inbox_id().to_string();
1347        let registration_cursor = self
1348            .identity_updates()
1349            .with_own_installation_events(
1350                &self.context.db(),
1351                Box::pin(async {
1352                    // Step 4: Publish identity update (makes installation visible)
1353                    let registration_cursor = crate::identity_updates::publish_with_conflict_retry(
1354                        self.context.api(),
1355                        &self.context.db(),
1356                        identity_update,
1357                        &self.context.scw_verifier(),
1358                    )
1359                    .await
1360                    .map_err(|error| IdentityUpdateError::Publish(Box::new(error)))?;
1361                    // Step 5: Fetch and store in local DB (needed for group operations)
1362                    retry_async!(
1363                        Retry::default(),
1364                        (async {
1365                            load_identity_updates(
1366                                self.context.api(),
1367                                &self.context.db(),
1368                                &[inbox_id.as_str()],
1369                            )
1370                            .await
1371                        })
1372                    )?;
1373                    Ok(registration_cursor)
1374                }),
1375            )
1376            .await?;
1377
1378        // Backend publication order can differ from local generation order.
1379        crate::state_tx::state_write(self.context.mls_storage(), |tx| {
1380            let storage = tx.storage();
1381            storage.db().record_key_package_publication(
1382                history_id,
1383                xmtp_proto::types::Cursor(key_package_cursor.sequence_id),
1384            )?;
1385            storage
1386                .db()
1387                .reset_key_package_rotation_queue(KEY_PACKAGE_ROTATION_INTERVAL_NS)?;
1388            Ok::<_, StorageError>(Continue(()))
1389        })
1390        .map(TransactionOutcome::into_continued)?;
1391
1392        // Mark identity as ready
1393        let mut stored_identity = StoredIdentity::try_from(self.identity())?;
1394        stored_identity.registration_cursor_sequence_id = Some(registration_cursor.0 as i64);
1395        stored_identity.store(&self.context.db())?;
1396        self.context
1397            .registration_event_pending()
1398            .store(true, Ordering::Release);
1399        self.identity().set_ready();
1400        self.ensure_registration_visible().await
1401    }
1402
1403    /// Whether registration has completed its visibility confirmation.
1404    /// A stored identity with a pending receipt is not yet registered.
1405    pub fn is_registration_visible(&self) -> Result<bool, ClientError> {
1406        let stored: Option<StoredIdentity> = self.context.db().fetch(&())?;
1407        Ok(self.identity().is_ready()
1408            && stored.is_some_and(|identity| identity.registration_cursor_sequence_id.is_none()))
1409    }
1410
1411    /// Confirm a stored registration before reporting success.
1412    pub async fn ensure_registration_visible(&self) -> Result<(), ClientError> {
1413        let stored: Option<StoredIdentity> = self.context.db().fetch(&())?;
1414        if stored.is_some_and(|identity| identity.registration_cursor_sequence_id.is_some()) {
1415            self.wait_for_registration_visible(VisibilityConfirmationOptions::default())
1416                .await?;
1417        }
1418        Ok(())
1419    }
1420
1421    /// Wait until the serving database exposes the registration identity-topic head.
1422    /// A missing or older head is polled. The timeout also bounds each request.
1423    pub async fn wait_for_registration_visible(
1424        &self,
1425        options: VisibilityConfirmationOptions,
1426    ) -> Result<(), ClientError> {
1427        use xmtp_common::time::{Duration, sleep, timeout};
1428        if !self.identity().is_ready() {
1429            return Err(ClientError::RegistrationNotVisible);
1430        }
1431        let stored: Option<StoredIdentity> = self.context.db().fetch(&())?;
1432        let stored = stored.ok_or(ClientError::RegistrationNotVisible)?;
1433        let Some(receipt) = stored.registration_cursor_sequence_id else {
1434            return Ok(());
1435        };
1436        self.context.server_configuration().check()?;
1437        let sequence_id = u64::try_from(receipt)
1438            .ok()
1439            .filter(|sequence_id| *sequence_id != 0)
1440            .ok_or(ClientError::RegistrationNotVisible)?;
1441        timeout(Duration::from_millis(options.timeout_ms), async {
1442            let mut delay = REGISTRATION_INITIAL_BACKOFF;
1443            let inbox = hex::decode(self.inbox_id())
1444                .map_err(|_| xmtp_api::ApiError::InvalidRequest("registration inbox id"))?;
1445            let topic = xmtp_proto::types::Topic::new_identity_update(inbox);
1446            xmtp_proto::types::Topic::parse(&topic)?;
1447            loop {
1448                let heads = self
1449                    .context
1450                    .api()
1451                    .newest_topic_cursors(vec![topic.clone()])
1452                    .await?;
1453                let head = heads
1454                    .get(&topic)
1455                    .ok_or(xmtp_api::ApiError::InvalidResponse(
1456                        "registration identity head",
1457                    ))?;
1458                // implements: IDENT-072
1459                if head.0 >= sequence_id {
1460                    return Ok::<(), ClientError>(());
1461                }
1462                sleep(delay).await;
1463                delay = (delay * 2).min(REGISTRATION_MAX_BACKOFF);
1464            }
1465        })
1466        .await
1467        .map_err(|_| ClientError::RegistrationNotVisible)??;
1468        if self
1469            .context
1470            .registration_event_pending()
1471            .swap(false, Ordering::AcqRel)
1472        {
1473            xmtp_events::EventWriter::emit(
1474                self.context.events(),
1475                Some(xmtp_events::ClientEvent::IdentityRegistered(
1476                    xmtp_events::IdentityRegistered {
1477                        inbox_id: self.inbox_id().to_string(),
1478                        installation_key: self.installation_id.to_vec(),
1479                    },
1480                )),
1481                None,
1482            );
1483        }
1484        self.context.db().clear_registration_cursor(receipt)?;
1485        Ok(())
1486    }
1487
1488    /// If no key rotation is scheduled, queue it to occur in the next 5 seconds.
1489    pub fn queue_key_rotation(&self) -> Result<(), ClientError> {
1490        crate::worker::key_package_maintenance::queue_key_rotation(&self.context)?;
1491        Ok(())
1492    }
1493
1494    /// Upload a new key package to the network replacing an existing key package.
1495    pub async fn rotate_and_upload_key_package(&self) -> Result<(), ClientError> {
1496        // A client with a blocked connection publishes nothing. This one
1497        // reaches `Identity` directly instead of going through a group or the
1498        // publish path, so it carries its own gate.
1499        self.ensure_identity_ready::<ClientError>()?;
1500        crate::identity_lifecycle::rotate_and_upload_key_package(
1501            self.identity(),
1502            self.context.api(),
1503            self.context.mls_storage(),
1504            CREATE_PQ_KEY_PACKAGE_EXTENSION,
1505        )
1506        .await?;
1507        // The rotation marked superseded KPs delete_at=now+grace; without this
1508        // the parked KpDeletion task would sweep them up to ~30d late.
1509        crate::worker::key_package_maintenance::nudge_deletion(&self.context)?;
1510
1511        Ok(())
1512    }
1513
1514    /// Fetches the current key package from the network for each of the `installation_id`s specified
1515    #[tracing::instrument(skip_all)]
1516    pub async fn get_key_packages_for_installation_ids(
1517        &self,
1518        installation_ids: Vec<Vec<u8>>,
1519    ) -> Result<
1520        HashMap<Vec<u8>, Result<VerifiedKeyPackageV2, KeyPackageVerificationError>>,
1521        ClientError,
1522    > {
1523        MlsStore::new(self.context.clone())
1524            .get_key_packages_for_installation_ids(installation_ids)
1525            .await
1526            .map_err(Into::into)
1527    }
1528
1529    /// Download all unread welcome messages and converts to a group struct, ignoring malformed messages.
1530    /// Returns any new groups created in the operation
1531    #[tracing::instrument(skip_all)]
1532    pub async fn sync_welcomes(&self) -> Result<Vec<MlsGroup<Context>>, GroupError> {
1533        self.ensure_identity_ready::<GroupError>()?;
1534        WelcomeService::new(self.context.clone())
1535            .sync_welcomes()
1536            .await
1537            .map_err(|error| self.context.server_configuration().blocked_or(error))
1538    }
1539
1540    /// Sync all groups for the current installation and return the number of groups that were synced.
1541    /// Only active groups will be synced.
1542    #[tracing::instrument(err, skip_all, fields(operation = "sync_all_groups"))]
1543    pub async fn sync_all_groups(
1544        &self,
1545        groups: Vec<MlsGroup<Context>>,
1546    ) -> Result<GroupSyncSummary, GroupError> {
1547        self.ensure_identity_ready::<GroupError>()?;
1548        WelcomeService::new(self.context.clone())
1549            .sync_all_groups(groups)
1550            .await
1551            .map_err(|error| self.context.server_configuration().blocked_or(error))
1552    }
1553
1554    /// Sync all unread welcome messages and then sync all groups.
1555    /// Returns the total number of active groups synced.
1556    #[xmtp_common::mls_span]
1557    pub async fn sync_all_welcomes_and_groups(
1558        &self,
1559        consent_states: Option<Vec<ConsentState>>,
1560    ) -> Result<GroupSyncSummary, GroupError> {
1561        self.ensure_identity_ready::<GroupError>()?;
1562        WelcomeService::new(self.context.clone())
1563            .sync_all_welcomes_and_groups(consent_states)
1564            .await
1565            .map_err(|error| self.context.server_configuration().blocked_or(error))
1566    }
1567
1568    /// Sweep every group flagged `paused_for_version` and clear the
1569    /// pause flag for any whose floor is now satisfied by this
1570    /// client's `pkg_version`. Pure local-state operation — no
1571    /// network calls. Returns the count of groups unstuck.
1572    ///
1573    /// `sync_all_welcomes_and_groups` already runs this sweep as a
1574    /// preamble; the standalone entry point is for SDKs that want a
1575    /// cheap "post-upgrade recovery" hook independent of the normal
1576    /// sync flow.
1577    pub async fn unstick_paused_groups(&self) -> Result<usize, GroupError> {
1578        self.ensure_identity_ready::<GroupError>()?;
1579        WelcomeService::new(self.context.clone())
1580            .unstick_paused_groups()
1581            .await
1582    }
1583
1584    pub async fn sync_all_welcomes_and_device_sync_groups(
1585        &self,
1586    ) -> Result<GroupSyncSummary, ClientError> {
1587        self.sync_welcomes().await?;
1588        self.sync_all_device_sync_groups().await
1589    }
1590
1591    pub async fn sync_all_device_sync_groups(&self) -> Result<GroupSyncSummary, ClientError> {
1592        let groups = self
1593            .context
1594            .db()
1595            .all_sync_groups()?
1596            .into_iter()
1597            .map(|g| {
1598                MlsGroup::new(
1599                    self.context.clone(),
1600                    g.id,
1601                    g.dm_id,
1602                    g.conversation_type,
1603                    g.created_at_ns,
1604                )
1605            })
1606            .collect();
1607
1608        Ok(self.sync_all_groups(groups).await?)
1609    }
1610
1611    /**
1612     * Validates a credential against the given installation public key
1613     *
1614     * This will go to the network and get the latest association state for the inbox.
1615     * It ensures that the installation_pub_key is in that association state
1616     */
1617    pub async fn validate_credential_against_network(
1618        &self,
1619        conn: &DbConnection<<Context::Db as XmtpDb>::Connection>,
1620        credential: &[u8],
1621        installation_pub_key: Vec<u8>,
1622    ) -> Result<InboxId, ClientError> {
1623        let inbox_id = parse_credential(credential)?;
1624        let association_state = IdentityUpdates::new(&self.context)
1625            .get_latest_association_state(conn, &inbox_id)
1626            .await?;
1627        let ident = MemberIdentifier::installation(installation_pub_key);
1628
1629        match association_state.get(&ident) {
1630            Some(_) => Ok(inbox_id),
1631            None => Err(IdentityError::InstallationIdNotFound(inbox_id).into()),
1632        }
1633    }
1634
1635    /// Reports whether the backend names an inbox for each identifier.
1636    /// This is an unverified availability hint. The client does not independently
1637    /// verify the lookup against current association state or check
1638    /// key packages. This is not a delivery guarantee.
1639    ///
1640    /// Arguments:
1641    /// - account_identifier: a list of account identifiers to check
1642    ///
1643    /// Returns:
1644    /// A map from each identifier to its availability hint.
1645    pub async fn can_message(
1646        &self,
1647        account_identifiers: &[Identifier],
1648    ) -> Result<HashMap<Identifier, bool>, ClientError> {
1649        // A client with a blocked connection issues no request. The blocked connection
1650        // alone, not `ensure_identity_ready`, because this answers before the
1651        // caller has registered an identity.
1652        self.context.server_configuration().check()?;
1653        let requests = account_identifiers.iter().map(Into::into).collect();
1654
1655        let results = self.context.api().get_inbox_ids(requests).await?;
1656        Ok(account_identifiers
1657            .iter()
1658            .cloned()
1659            .zip(results.into_iter().map(|inbox_id| inbox_id.is_some()))
1660            .collect())
1661    }
1662}
1663
1664impl<Context: XmtpSharedContext> Client<Context> {
1665    pub fn attachments(&self) -> Attachments<Context> {
1666        Attachments {
1667            context: self.context.clone(),
1668        }
1669    }
1670}
1671
1672impl<Context: XmtpSharedContext> Client<Context> {
1673    /// Store configuration, then register inline. The task retries transient errors.
1674    pub async fn enable_notifications(
1675        &self,
1676        config: NotificationConfig,
1677    ) -> Result<NotificationState, NotificationError> {
1678        notifications::enable_notifications(&self.context, config).await
1679    }
1680
1681    /// Disable locally before unregistering. The recipient identity and overrides stay.
1682    /// A failed unregister leaves the client disabled with its recipient reset owed.
1683    pub async fn disable_notifications(&self) -> Result<(), NotificationError> {
1684        notifications::disable_notifications(&self.context).await
1685    }
1686
1687    /// Read the locally stored state without a backend call.
1688    pub fn notification_state(&self) -> Result<NotificationState, StorageError> {
1689        notifications::notification_state(&self.context)
1690    }
1691}
1692
1693impl<Context: XmtpSharedContext> Client<Context> {
1694    /// The inbox that holds each handle, in order, or `None`. An answer from
1695    /// the backend counts only when the newest state the client derives for
1696    /// the answered inbox holds the handle. An old handle of an inbox still
1697    /// resolves to it.
1698    pub async fn resolve_handles(
1699        &self,
1700        handles: &[Handle],
1701    ) -> Result<Vec<Option<InboxId>>, ClientError> {
1702        Ok(inbox_handles::resolve_verified_in(
1703            &self.context,
1704            &inbox_handles::handle_targets(handles),
1705        )
1706        .await?)
1707    }
1708
1709    /// Whether the backend names an inbox for each handle, in order. This
1710    /// checks no association state: use it only where a wrong answer costs a
1711    /// candidate, never to reach an inbox.
1712    pub async fn handles_have_owner(&self, handles: &[Handle]) -> Result<Vec<bool>, ClientError> {
1713        Ok(inbox_handles::handles_have_owner(&self.context, handles).await?)
1714    }
1715
1716    /// The handles for `input` that no inbox holds. `input` is a handle as a
1717    /// user typed it; its ASCII capitals become lowercase. Input with a code
1718    /// checks only that handle. A name without a code checks the name and
1719    /// [`HANDLE_SEARCH_CODES`](inbox_handles::HANDLE_SEARCH_CODES) distinct random codes in one lookup, and the
1720    /// name comes first in the result. Text outside the handle grammar fails
1721    /// before any request. No policy applies here, so a returned handle can
1722    /// still be refused when it is claimed.
1723    pub async fn available_handles(&self, input: &str) -> Result<Vec<Handle>, ClientError> {
1724        Ok(inbox_handles::available_handles(&self.context, input).await?)
1725    }
1726
1727    /// Claim `handle` for this client's inbox, signed by this installation's
1728    /// key. A claim of a handle the inbox already holds makes it the display
1729    /// handle again. The backend refuses a taken, reserved, short, or too
1730    /// frequent claim with `ApiError::HandleRejected`. After the publish, the
1731    /// client has stored the inbox's new identity update.
1732    pub async fn claim_handle(&self, handle: &Handle) -> Result<(), ClientError> {
1733        self.ensure_identity_ready::<ClientError>()?;
1734        Ok(inbox_handles::claim_handle(&self.context, handle).await?)
1735    }
1736
1737    /// The inbox of each target, in order, from one call. Fails when any
1738    /// target has no inbox: an account as `find_or_create_dm_by_identity`
1739    /// does, and a handle with [`HandleError::NotFound`].
1740    pub async fn resolve_targets(
1741        &self,
1742        targets: &[IdentityTarget],
1743    ) -> Result<Vec<InboxId>, ClientError> {
1744        Ok(inbox_handles::resolve_targets(&self.context, targets).await?)
1745    }
1746
1747    /// The active DM with `target`, or `None`. A target that no inbox holds
1748    /// has no DM.
1749    pub async fn find_dm_by_target(
1750        &self,
1751        target: &IdentityTarget,
1752    ) -> Result<Option<MlsGroup<Context>>, ClientError> {
1753        let Some(peer) =
1754            inbox_handles::resolve_verified_in(&self.context, std::slice::from_ref(target))
1755                .await?
1756                .pop()
1757                .flatten()
1758        else {
1759            return Ok(None);
1760        };
1761        let stored = self.context.db().find_active_dm_group(&DmMembers {
1762            member_one_inbox_id: self.inbox_id(),
1763            member_two_inbox_id: peer.as_str(),
1764        })?;
1765        stored.map(|stored| self.group(&stored.id)).transpose()
1766    }
1767
1768    /// Find the DM with `target`, or create it. A handle resolves before the
1769    /// existing DM is looked up.
1770    pub async fn find_or_create_dm_by_target(
1771        &self,
1772        target: &IdentityTarget,
1773        opts: DMMetadataOptions,
1774    ) -> Result<MlsGroup<Context>, ClientError> {
1775        match target {
1776            IdentityTarget::Account(identifier) => {
1777                self.find_or_create_dm_by_identity(identifier.clone(), Some(opts))
1778                    .await
1779            }
1780            IdentityTarget::Handle(handle) => {
1781                let peer = self
1782                    .resolve_handles(std::slice::from_ref(handle))
1783                    .await?
1784                    .pop()
1785                    .flatten()
1786                    .ok_or(HandleError::NotFound)?;
1787                self.find_or_create_dm(peer, Some(opts)).await
1788            }
1789        }
1790    }
1791
1792    /// Create a group with `targets` as its members, in one commit. Targets,
1793    /// member identities, and key packages resolve before the group is stored.
1794    /// A target with no inbox leaves no group: a handle fails with
1795    /// [`HandleError::NotFound`], an account with [`GroupError::AddressNotFound`].
1796    pub async fn create_group_with_targets(
1797        &self,
1798        targets: &[IdentityTarget],
1799        permissions_policy_set: Option<PolicySet>,
1800        opts: Option<GroupMetadataOptions>,
1801    ) -> Result<MlsGroup<Context>, ClientError> {
1802        let inboxes = inbox_handles::resolve_targets_in(&self.context, targets).await?;
1803        let inbox_ids = crate::groups::targets::require_account_inboxes(targets, inboxes)?;
1804        self.create_group_with_members(&inbox_ids, permissions_policy_set, opts)
1805            .await
1806    }
1807}
1808
1809impl<Context> Client<Context>
1810where
1811    Context: XmtpSharedContext + 'static,
1812{
1813    /// Use push metadata as a target. Fetch its complete prefix before processing.
1814    pub async fn process_streamed_welcome_message(
1815        &self,
1816        envelope_bytes: Vec<u8>,
1817    ) -> Result<Vec<MlsGroup<Context>>, SubscribeError> {
1818        subscriptions::process_streamed_welcome_message(&self.context, envelope_bytes).await
1819    }
1820
1821    pub async fn stream_conversations(
1822        &self,
1823        conversation_type: Option<ConversationType>,
1824        include_duplicate_dms: bool,
1825    ) -> Result<
1826        impl Stream<Item = Result<MlsGroup<Context>, SubscribeError>> + use<'_, Context>,
1827        SubscribeError,
1828    >
1829    where
1830        Context::ApiClient: XmtpMlsStreams,
1831    {
1832        subscriptions::stream_conversations(&self.context, conversation_type, include_duplicate_dms)
1833            .await
1834    }
1835
1836    /// Stream conversations but decouple the lifetime of 'self' from the stream.
1837    pub async fn stream_conversations_owned(
1838        &self,
1839        conversation_type: Option<ConversationType>,
1840        include_duplicate_dms: bool,
1841    ) -> Result<
1842        impl Stream<Item = Result<MlsGroup<Context>, SubscribeError>> + 'static + use<Context>,
1843        SubscribeError,
1844    >
1845    where
1846        Context::ApiClient: XmtpMlsStreams,
1847    {
1848        subscriptions::stream_conversations_owned(
1849            self.context.clone(),
1850            conversation_type,
1851            include_duplicate_dms,
1852        )
1853        .await
1854    }
1855}
1856
1857impl<Context> Client<Context>
1858where
1859    Context: XmtpSharedContext + 'static,
1860    Context::ApiClient: XmtpMlsStreams + 'static,
1861    Context::MlsStorage: 'static,
1862{
1863    pub fn stream_conversations_with_callback(
1864        client: Arc<Client<Context>>,
1865        conversation_type: Option<ConversationType>,
1866        convo_callback: impl FnMut(Result<MlsGroup<Context>, SubscribeError>) + MaybeSend + 'static,
1867        on_close: impl FnOnce() + MaybeSend + 'static,
1868        include_duplicate_dms: bool,
1869    ) -> impl StreamHandle<StreamOutput = Result<(), SubscribeError>> {
1870        subscriptions::stream_conversations_with_callback(
1871            client.context.clone(),
1872            client,
1873            conversation_type,
1874            convo_callback,
1875            on_close,
1876            include_duplicate_dms,
1877        )
1878    }
1879
1880    pub async fn stream_all_messages(
1881        &self,
1882        conversation_type: Option<ConversationType>,
1883        consent_state: Option<Vec<ConsentState>>,
1884    ) -> Result<impl Stream<Item = Result<StoredGroupMessage, SubscribeError>> + '_, SubscribeError>
1885    {
1886        subscriptions::stream_all_messages(&self.context, conversation_type, consent_state).await
1887    }
1888
1889    pub async fn stream_all_messages_owned(
1890        &self,
1891        conversation_type: Option<ConversationType>,
1892        consent_state: Option<Vec<ConsentState>>,
1893    ) -> Result<
1894        impl Stream<Item = Result<StoredGroupMessage, SubscribeError>> + 'static + use<Context>,
1895        SubscribeError,
1896    > {
1897        subscriptions::stream_all_messages_owned(
1898            self.context.clone(),
1899            conversation_type,
1900            consent_state,
1901        )
1902        .await
1903    }
1904
1905    pub fn stream_consent_with_callback(
1906        client: Arc<Client<Context>>,
1907        callback: impl FnMut(Result<Vec<StoredConsentRecord>, SubscribeError>) + MaybeSend + 'static,
1908        on_close: impl FnOnce() + MaybeSend + 'static,
1909    ) -> impl StreamHandle<StreamOutput = Result<(), SubscribeError>> {
1910        subscriptions::stream_consent_with_callback(
1911            client.context.clone(),
1912            client,
1913            callback,
1914            on_close,
1915        )
1916    }
1917
1918    pub fn stream_preferences_with_callback(
1919        client: Arc<Client<Context>>,
1920        callback: impl FnMut(Result<Vec<PreferenceUpdate>, SubscribeError>) + MaybeSend + 'static,
1921        on_close: impl FnOnce() + MaybeSend + 'static,
1922    ) -> impl StreamHandle<StreamOutput = Result<(), SubscribeError>> {
1923        subscriptions::stream_preferences_with_callback(
1924            client.context.clone(),
1925            client,
1926            callback,
1927            on_close,
1928        )
1929    }
1930
1931    pub fn stream_message_deletions_with_callback(
1932        client: Arc<Client<Context>>,
1933        callback: impl FnMut(Result<DecodedMessage, SubscribeError>) + MaybeSend + 'static,
1934        on_close: impl FnOnce() + MaybeSend + 'static,
1935    ) -> impl StreamHandle<StreamOutput = Result<(), SubscribeError>> {
1936        subscriptions::stream_message_deletions_with_callback(
1937            client.context.clone(),
1938            client,
1939            callback,
1940            on_close,
1941        )
1942    }
1943}
1944
1945impl<Context> Client<Context>
1946where
1947    Context: XmtpSharedContext + 'static,
1948    Context::ApiClient: XmtpMlsStreams + 'static,
1949    Context::MlsStorage: 'static,
1950    <Context::ApiClient as XmtpMlsStreams>::GroupMessageStream: Unpin,
1951    <Context::ApiClient as XmtpMlsStreams>::WelcomeMessageStream: Unpin,
1952{
1953    #[cfg(any(test, feature = "test-utils"))]
1954    pub async fn stream_all_messages_owned_with_stats(
1955        &self,
1956        conversation_type: Option<ConversationType>,
1957        consent_state: Option<Vec<ConsentState>>,
1958    ) -> Result<
1959        impl StreamWithStats<Item = Result<StoredGroupMessage, SubscribeError>> + 'static,
1960        SubscribeError,
1961    > {
1962        subscriptions::stream_all_messages_owned_with_stats(
1963            self.context.clone(),
1964            conversation_type,
1965            consent_state,
1966        )
1967        .await
1968    }
1969}
1970
1971impl<C: XmtpSharedContext + 'static> Client<C> {
1972    /// Process fixed starting targets and only the discoveries from the enrolled Welcomes.
1973    pub async fn catch_up_to_live(
1974        &self,
1975        timeout: Option<xmtp_common::time::Duration>,
1976    ) -> Result<CatchUpSummary, CatchUpError> {
1977        subscriptions::catch_up::catch_up_to_live(&self.context, timeout).await
1978    }
1979}
1980
1981#[cfg(not(target_arch = "wasm32"))]
1982impl<C> Client<C>
1983where
1984    C: XmtpSharedContext + 'static,
1985    C::ApiClient:
1986        XmtpMlsBidiStreams + XmtpMlsStreams + ApiClientIdentity + Clone + Send + Sync + 'static,
1987    <C::ApiClient as XmtpMlsBidiStreams>::SubscribeStream: 'static,
1988{
1989    pub fn stream_all_messages_with_callback_dispatch(
1990        client: Arc<Client<C>>,
1991        conversation_type: Option<ConversationType>,
1992        consent_states: Option<Vec<ConsentState>>,
1993        callback: impl FnMut(Result<StoredGroupMessage, SubscribeError>) + MaybeSend + 'static,
1994        on_close: impl FnOnce() + MaybeSend + 'static,
1995    ) -> impl StreamHandle<StreamOutput = Result<(), SubscribeError>> {
1996        router_callbacks::stream_all_messages_with_callback_dispatch(
1997            client.context.clone(),
1998            client,
1999            conversation_type,
2000            consent_states,
2001            callback,
2002            on_close,
2003        )
2004    }
2005
2006    pub fn stream_conversations_with_callback_dispatch(
2007        client: Arc<Client<C>>,
2008        conversation_type: Option<ConversationType>,
2009        include_duplicate_dms: bool,
2010        callback: impl FnMut(Result<MlsGroup<C>, SubscribeError>) + MaybeSend + 'static,
2011        on_close: impl FnOnce() + MaybeSend + 'static,
2012    ) -> impl StreamHandle<StreamOutput = Result<(), SubscribeError>> {
2013        router_callbacks::stream_conversations_with_callback_dispatch(
2014            client.context.clone(),
2015            client,
2016            conversation_type,
2017            include_duplicate_dms,
2018            callback,
2019            on_close,
2020        )
2021    }
2022}
2023
2024#[cfg(test)]
2025pub(crate) mod tests;
2026
2027#[cfg(all(test, not(target_arch = "wasm32")))]
2028pub(crate) mod test_hooks {
2029    use std::sync::Arc;
2030    use tokio::sync::Barrier;
2031
2032    type PauseHook = (Vec<u8>, Arc<Barrier>, Arc<Barrier>);
2033    /// Stops the next `reconnect_db` of one installation after its closed
2034    /// check. The test runs this synchronous call on a blocking thread.
2035    pub(crate) static PAUSE_RECONNECT: parking_lot::Mutex<Option<PauseHook>> =
2036        parking_lot::Mutex::new(None);
2037
2038    pub(super) fn pause_reconnect(installation_id: &[u8]) {
2039        let paused = {
2040            let mut hook = PAUSE_RECONNECT.lock();
2041            if hook
2042                .as_ref()
2043                .is_some_and(|(installation, _, _)| installation.as_slice() == installation_id)
2044            {
2045                hook.take()
2046            } else {
2047                None
2048            }
2049        };
2050        if let Some((_, entered, release)) = paused {
2051            tokio::runtime::Handle::current().block_on(async {
2052                entered.wait().await;
2053                release.wait().await;
2054            });
2055        }
2056    }
2057}