1#[cfg(test)]
2use crate::GroupCommitLock;
3use crate::{
4 StorageError, XmtpApi,
5 client::{Client, ClientError, DeviceSync},
6 context::{XmtpMlsLocalContext, XmtpSharedContext},
7 groups::change_callbacks::UnstableChangeCallbacks,
8 identity::{Identity, IdentityStrategy},
9 identity_updates::load_identity_updates,
10 mutex_registry::MutexRegistry,
11 server_configuration::ServerConfigurationHandle,
12 utils::{VersionInfo, cleanup_duplicate_updates},
13 worker::{WorkerRunner, tasks::TaskWorker},
14 worker::{device_sync::worker::SyncWorker, disappearing_messages::DisappearingMessagesWorker},
15};
16use futures::FutureExt;
17use std::sync::Arc;
18use std::sync::atomic::AtomicBool;
19use thiserror::Error;
20use tokio::sync::broadcast;
21use tokio_util::sync::CancellationToken;
22use tracing::debug;
23use xmtp_api::ApiClientWrapper;
24use xmtp_api_backend::TrackedStatsClient;
25use xmtp_common::{ErrorCode, Event, Retry};
26use xmtp_cryptography::signature::IdentifierValidationError;
27use xmtp_db::{DbConnection, XmtpMlsStorageProvider, prelude::*};
28use xmtp_db::{XmtpDb, sql_key_store::SqlKeyStore};
29use xmtp_id::scw_verifier::SmartContractSignatureVerifier;
30use xmtp_macro::log_event;
31use xmtp_proto::xmtp::mls::database::{
32 ProcessPendingSelfRemove, Task as TaskProto, task::Task as TaskKind,
33};
34
35type ContextParts<Api, S, Db> = Arc<XmtpMlsLocalContext<Api, Db, S>>;
36
37#[derive(Error, Debug, ErrorCode)]
38pub enum ClientBuilderError {
39 #[error(transparent)]
40 #[error_code(inherit)]
41 AddressValidation(#[from] IdentifierValidationError),
42 #[error("Missing parameter: {parameter}")]
46 MissingParameter { parameter: &'static str },
47 #[error(transparent)]
51 ClientError(#[from] crate::client::ClientError),
52 #[error("Storage Error")]
56 StorageError(#[from] StorageError),
57 #[error(transparent)]
61 Identity(#[from] crate::identity::IdentityError),
62 #[error(transparent)]
66 WrappedApiError(#[from] xmtp_api::ApiError),
67 #[error(transparent)]
71 GroupError(#[from] Box<crate::groups::GroupError>),
72 #[error(transparent)]
76 DeviceSync(#[from] Box<crate::worker::device_sync::DeviceSyncError>),
77 #[error("Offline build failed, builder tried to access the network")]
81 OfflineBuildFailed,
82}
83
84impl From<crate::worker::device_sync::DeviceSyncError> for ClientBuilderError {
85 fn from(value: crate::worker::device_sync::DeviceSyncError) -> Self {
86 ClientBuilderError::DeviceSync(Box::new(value))
87 }
88}
89
90impl From<crate::groups::GroupError> for ClientBuilderError {
91 fn from(value: crate::groups::GroupError) -> Self {
92 ClientBuilderError::GroupError(Box::new(value))
93 }
94}
95
96pub struct ClientBuilder<ApiClient, S, Db = xmtp_db::DefaultStore> {
97 pub(crate) api_client: Option<ApiClient>,
98 pub(crate) identity: Option<Identity>,
99 pub(crate) store: Option<Db>,
100 pub(crate) identity_strategy: IdentityStrategy,
101 pub(crate) scw_verifier: Option<Box<dyn SmartContractSignatureVerifier>>,
102 pub(crate) custom_scw_verifier: bool,
105 pub(crate) device_sync_worker_mode: DeviceSyncMode,
106 pub(crate) fork_recovery_opts: Option<ForkRecoveryOpts>,
107 pub(crate) change_callbacks: UnstableChangeCallbacks,
109 pub(crate) stream_policy: crate::subscriptions::policy::StreamPolicy,
110 pub(crate) incoming_factory:
111 Option<Arc<dyn crate::subscriptions::incoming::SubscriptionFactory>>,
112 pub(crate) version_info: VersionInfo,
113 pub(crate) allow_offline: bool,
114 pub(crate) disable_commit_log_worker: bool,
115 pub(crate) mls_storage: Option<S>,
116 pub(crate) disable_workers: bool,
117 pub(crate) worker_config: crate::worker::WorkerConfig,
118 pub(crate) config_provider: Option<Arc<dyn xmtp_configuration::ConfigProvider>>,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub enum DeviceSyncMode {
126 Disabled,
127 Enabled,
128}
129
130#[derive(Clone, Debug, PartialEq)]
131pub enum ForkRecoveryPolicy {
132 None,
133 AllowlistedGroups,
134 All,
135}
136
137#[derive(Clone, Debug)]
138pub struct ForkRecoveryOpts {
139 pub enable_recovery_requests: ForkRecoveryPolicy,
140 pub groups_to_request_recovery: Vec<String>,
141 pub disable_recovery_responses: bool,
142 pub worker_interval_ns: Option<u64>,
143}
144
145impl Default for ForkRecoveryOpts {
146 fn default() -> Self {
147 Self {
148 enable_recovery_requests: ForkRecoveryPolicy::None,
149 groups_to_request_recovery: Vec::new(),
150 disable_recovery_responses: false,
151 worker_interval_ns: None,
152 }
153 }
154}
155
156impl Client<()> {
157 pub fn builder(strategy: IdentityStrategy) -> ClientBuilder<(), ()> {
159 ClientBuilder::<(), ()>::new(strategy)
160 }
161}
162
163impl<ApiClient, S, Db> ClientBuilder<ApiClient, S, Db> {
164 #[cfg(test)]
166 pub(crate) fn stream_policy(
167 mut self,
168 settings: crate::subscriptions::policy::StreamPolicy,
169 ) -> Self {
170 self.stream_policy = settings;
171 self
172 }
173
174 #[tracing::instrument(level = "trace", skip_all)]
175 pub fn new(identity_strategy: IdentityStrategy) -> Self {
176 Self {
177 identity_strategy,
178 api_client: None,
179 identity: None,
180 store: None,
181 scw_verifier: None,
182 custom_scw_verifier: false,
183 device_sync_worker_mode: DeviceSyncMode::Enabled,
184 fork_recovery_opts: None,
185 change_callbacks: UnstableChangeCallbacks::default(),
186 stream_policy: crate::subscriptions::policy::StreamPolicy::default(),
187 incoming_factory: None,
188 version_info: VersionInfo::default(),
189 allow_offline: false,
190 disable_commit_log_worker: false,
191 mls_storage: None,
192 disable_workers: false,
193 worker_config: crate::worker::WorkerConfig::default(),
194 config_provider: None,
195 }
196 }
197}
198
199#[cfg(test)]
200impl<ApiClient, S, Db> ClientBuilder<ApiClient, S, Db>
201where
202 ApiClient: Clone,
203 Db: Clone,
204 S: Clone,
205{
206 pub fn from_client(
207 client: Client<ContextParts<ApiClient, S, Db>>,
208 ) -> ClientBuilder<ApiClient, S, Db> {
209 let cloned_api: ApiClient = client.context.api_client.clone().api_client;
210 ClientBuilder {
211 api_client: Some(cloned_api),
212 identity: Some(client.context.identity.clone()),
213 store: Some(client.context.store.clone()),
214 identity_strategy: IdentityStrategy::CachedOnly,
215 scw_verifier: Some(Box::new(client.context.scw_verifier.clone())),
216 custom_scw_verifier: false,
217 device_sync_worker_mode: client.context.device_sync.mode,
218 fork_recovery_opts: Some(client.context.fork_recovery_opts.clone()),
219 change_callbacks: client.context.change_callbacks.clone(),
220 stream_policy: client.context.incoming_runtime.policy().clone(),
221 incoming_factory: client.context.incoming_runtime.factory.clone(),
222 version_info: client.context.version_info.clone(),
223 allow_offline: false,
224 disable_commit_log_worker: false,
225 mls_storage: Some(client.context.mls_storage.clone()),
226 disable_workers: false,
227 worker_config: client.context.worker_config.clone(),
228 config_provider: None,
229 }
230 }
231}
232
233fn backfill_pending_self_remove_tasks<C>(context: &C) -> Result<(), StorageError>
241where
242 C: XmtpSharedContext,
243{
244 let db = context.db();
245 for raw_id in db.get_groups_have_pending_leave_request()? {
246 let Ok(group_id) = xmtp_proto::types::GroupId::try_from(raw_id.as_slice()) else {
247 continue;
248 };
249 let now = xmtp_common::time::now_ns();
250 let proto = TaskProto {
251 task: Some(TaskKind::ProcessPendingSelfRemove(
252 ProcessPendingSelfRemove {
253 group_id: group_id.to_vec(),
254 },
255 )),
256 };
257 let task = xmtp_db::tasks::NewTask::builder()
258 .originating_message_sequence_id(0)
259 .created_at_ns(now)
260 .next_attempt_at_ns(now)
261 .build(proto)?;
262 db.upsert_pending_self_remove_task(&group_id, task)?;
266 }
267 Ok(())
268}
269
270impl<ApiClient, S, Db> ClientBuilder<ApiClient, S, Db> {
273 pub fn build_offline(self) -> Result<Client<ContextParts<ApiClient, S, Db>>, ClientBuilderError>
276 where
277 ApiClient: XmtpApi + 'static,
278 Db: xmtp_db::XmtpDb + 'static,
279 S: XmtpMlsStorageProvider + 'static,
280 {
281 self.build()
282 .now_or_never()
283 .ok_or(ClientBuilderError::OfflineBuildFailed)
284 .flatten()
285 }
286
287 #[tracing::instrument(err, skip_all, fields(operation = "mls.build_client"))]
288 pub async fn build(self) -> Result<Client<ContextParts<ApiClient, S, Db>>, ClientBuilderError>
289 where
290 ApiClient: XmtpApi + 'static,
291 Db: xmtp_db::XmtpDb + 'static,
292 S: XmtpMlsStorageProvider + 'static,
293 {
294 let ClientBuilder {
295 mut api_client,
296 identity,
297 mut store,
298 identity_strategy,
299 mut scw_verifier,
300 custom_scw_verifier,
301
302 device_sync_worker_mode,
303 fork_recovery_opts,
304 change_callbacks,
305 stream_policy,
306 incoming_factory,
307 version_info,
308 allow_offline,
309 disable_commit_log_worker,
310 mut mls_storage,
311 disable_workers,
312 worker_config,
313 config_provider,
314 ..
315 } = self;
316
317 let api_client = api_client
318 .take()
319 .ok_or(ClientBuilderError::MissingParameter {
320 parameter: "api_client",
321 })?;
322
323 let scw_verifier = scw_verifier
324 .take()
325 .ok_or(ClientBuilderError::MissingParameter {
326 parameter: "scw_verifier",
327 })?;
328
329 let store = store
330 .take()
331 .ok_or(ClientBuilderError::MissingParameter { parameter: "store" })?;
332
333 let mls_storage = mls_storage
334 .take()
335 .ok_or(ClientBuilderError::MissingParameter {
336 parameter: "mls_storage",
337 })?;
338
339 let mut api_client = ApiClientWrapper::new(api_client, Retry::default());
340 let conn = store.db();
341
342 let has_config_provider = config_provider.is_some();
348 let server_configuration = match config_provider {
349 Some(provider) => ServerConfigurationHandle::new(provider),
350 None => crate::server_configuration::resolve(&api_client, &conn, allow_offline).await?,
351 };
352
353 let server_configuration = server_configuration.with_chain_restriction(custom_scw_verifier);
354
355 crate::server_configuration::check_minimum_version(
358 server_configuration.configuration(),
359 version_info.pkg_semver().semver(),
360 )?;
361
362 let configuration = server_configuration.configuration();
365 if configuration.auth.enabled && !api_client.has_credential_source() {
366 return Err(ClientBuilderError::ClientError(ClientError::AuthRequired {
367 required_scopes: configuration.auth.required_scopes.clone(),
368 }));
369 }
370
371 let snapshot = Arc::new(configuration.clone());
377 api_client
378 .api_client
379 .set_limits(Arc::new(snapshot.limits.clone()));
380 api_client.set_configuration(snapshot);
381
382 let mut identity = if let Some(identity) = identity {
383 identity
384 } else {
385 identity_strategy
386 .initialize_identity(&api_client, &mls_storage, &scw_verifier)
387 .await?
388 };
389
390 if let Some(request) = identity.signature_request.as_mut() {
394 server_configuration.restrict(request);
395 }
396
397 debug!(
398 inbox_id = identity.inbox_id(),
399 installation_id = hex::encode(identity.installation_keys.public_bytes()),
400 "Initialized identity"
401 );
402 if !allow_offline {
403 load_identity_updates(
405 &api_client,
406 &conn,
407 vec![identity.inbox_id.as_str()].as_slice(),
408 )
409 .await?;
410 }
411
412 let mut worker_config = worker_config;
417 if disable_workers {
418 for kind in [
420 crate::worker::WorkerKind::DeviceSync,
421 crate::worker::WorkerKind::DisappearingMessages,
422 crate::worker::WorkerKind::CommitLog,
423 crate::worker::WorkerKind::TaskRunner,
424 crate::worker::WorkerKind::ConfigurationRefresh,
425 ] {
426 worker_config.enabled.insert(kind, false);
427 }
428 }
429 if matches!(device_sync_worker_mode, DeviceSyncMode::Disabled) {
430 worker_config
431 .enabled
432 .entry(crate::worker::WorkerKind::DeviceSync)
433 .or_insert(false);
434 }
435 if disable_commit_log_worker {
436 worker_config
437 .enabled
438 .entry(crate::worker::WorkerKind::CommitLog)
439 .or_insert(false);
440 }
441
442 let (local_events, _) = broadcast::channel(32);
443 let (worker_tx, _) = broadcast::channel(32);
444 let mut workers = WorkerRunner::new();
445 let context = Arc::new(XmtpMlsLocalContext {
446 identity,
447 mls_storage,
448 store,
449 api_client,
450 version_info,
451 server_configuration,
452 scw_verifier: Arc::new(scw_verifier),
453 mutexes: MutexRegistry::new(),
454 #[cfg(test)]
455 mls_commit_lock: Arc::new(GroupCommitLock::new()),
456 local_events: local_events.clone(),
457 worker_events: worker_tx.clone(),
458 device_sync: DeviceSync {
459 mode: device_sync_worker_mode,
460 },
461 fork_recovery_opts: fork_recovery_opts.unwrap_or_default(),
462 change_callbacks,
463 incoming_runtime: Arc::new(crate::subscriptions::incoming::IncomingRuntime::new(
464 stream_policy,
465 incoming_factory,
466 )),
467 worker_config,
468
469 worker_metrics: workers.metrics().clone(),
470 task_channels: workers.task_channels().clone(),
471 disappearing_channels: crate::worker::disappearing_messages::DisappearingChannels::new(
472 ),
473 cancellation_token: CancellationToken::new(),
474 shutdown_complete: Arc::new(AtomicBool::new(false)),
475 delivery_owner: Default::default(),
476 identity_resolutions: Default::default(),
477 });
478
479 if !disable_workers {
481 use crate::worker::WorkerKind;
482 let enabled = |k| context.worker_config().worker_enabled(k);
484
485 if enabled(WorkerKind::DeviceSync) && context.device_sync_worker_enabled() {
488 workers.register_new_worker::<SyncWorker<ContextParts<ApiClient, S, Db>>, _>(
489 context.clone(),
490 );
491 }
492 if enabled(WorkerKind::DisappearingMessages) {
493 workers
494 .register_new_worker::<DisappearingMessagesWorker<ContextParts<ApiClient, S, Db>>, _>(
495 context.clone(),
496 );
497 }
498 if enabled(WorkerKind::CommitLog)
502 && context
503 .server_configuration()
504 .configuration()
505 .mls
506 .commit_log_enabled()
507 && !disable_commit_log_worker
508 {
509 workers.register_new_worker::<
510 crate::groups::commit_log::CommitLogWorker<ContextParts<ApiClient, S, Db>>,
511 _,
512 >(context.clone());
513 }
514 if enabled(WorkerKind::ConfigurationRefresh) && !has_config_provider {
517 workers
518 .register_new_worker::<crate::server_configuration::worker::ConfigurationWorker<
519 ContextParts<ApiClient, S, Db>,
520 >, _>(context.clone());
521 }
522 if enabled(WorkerKind::TaskRunner) {
523 workers.register_new_worker::<TaskWorker<ContextParts<ApiClient, S, Db>>, _>(
524 context.clone(),
525 );
526 crate::worker::key_package_maintenance::seed_and_reconcile_kp_tasks(&context)?;
534 if let Err(e) = backfill_pending_self_remove_tasks(&context) {
543 tracing::warn!(
544 "pending-self-remove backfill failed (will rely on next sync): {e}"
545 );
546 }
547 }
548 }
549
550 let workers = Arc::new(workers);
551
552 if !disable_workers {
553 workers.spawn(context.clone());
554 }
555
556 log_event!(
557 Event::ClientCreated,
558 context.installation_id(),
559 inbox_id = context.inbox_id(),
560 full_installation_id = hex::encode(context.installation_id()),
561 device_sync_enabled = context.device_sync_worker_enabled(),
562 disabled_workers = disable_workers,
563 );
564
565 let installation_id = context.installation_id();
566 let client = Client {
567 context,
568 installation_id,
569 local_events,
570 workers,
571 };
572
573 let conn = DbConnection::new(client.db());
575 let cancel = client.context.cancellation_token().clone();
576 xmtp_common::spawn(None, async move {
577 tokio::select! {
578 _ = cancel.cancelled() => {}
579 _ = cleanup_duplicate_updates::perform(conn) => {}
580 }
581 });
582
583 Ok(client)
584 }
585
586 pub fn identity(self, identity: Identity) -> Self {
587 Self {
588 identity: Some(identity),
589 ..self
590 }
591 }
592
593 pub fn unstable_change_callbacks(self, change_callbacks: UnstableChangeCallbacks) -> Self {
602 Self {
603 change_callbacks,
604 ..self
605 }
606 }
607
608 pub fn store<NewDb>(self, db: NewDb) -> ClientBuilder<ApiClient, S, NewDb> {
609 ClientBuilder {
610 store: Some(db),
611 api_client: self.api_client,
612 identity: self.identity,
613 identity_strategy: self.identity_strategy,
614 scw_verifier: self.scw_verifier,
615 custom_scw_verifier: self.custom_scw_verifier,
616 device_sync_worker_mode: self.device_sync_worker_mode,
617 fork_recovery_opts: self.fork_recovery_opts,
618 change_callbacks: self.change_callbacks,
619 stream_policy: self.stream_policy,
620 incoming_factory: self.incoming_factory,
621 version_info: self.version_info,
622 allow_offline: self.allow_offline,
623 disable_commit_log_worker: self.disable_commit_log_worker,
624 mls_storage: self.mls_storage,
625 disable_workers: self.disable_workers,
626 worker_config: self.worker_config,
627 config_provider: self.config_provider,
628 }
629 }
630
631 pub fn default_mls_store(
633 self,
634 ) -> Result<
635 ClientBuilder<ApiClient, SqlKeyStore<<Db as XmtpDb>::DbQuery>, Db>,
636 ClientBuilderError,
637 >
638 where
639 Db: XmtpDb,
640 {
641 Ok(ClientBuilder {
642 api_client: self.api_client,
643 identity: self.identity,
644 identity_strategy: self.identity_strategy,
645 scw_verifier: self.scw_verifier,
646 custom_scw_verifier: self.custom_scw_verifier,
647 device_sync_worker_mode: self.device_sync_worker_mode,
648 fork_recovery_opts: self.fork_recovery_opts,
649 change_callbacks: self.change_callbacks,
650 stream_policy: self.stream_policy,
651 incoming_factory: self.incoming_factory,
652 version_info: self.version_info,
653 allow_offline: self.allow_offline,
654 disable_commit_log_worker: self.disable_commit_log_worker,
655 mls_storage: Some(SqlKeyStore::new(
656 self.store
657 .as_ref()
658 .ok_or(ClientBuilderError::MissingParameter {
659 parameter: "encrypted store",
660 })?
661 .db(),
662 )),
663 store: self.store,
664 disable_workers: self.disable_workers,
665 worker_config: self.worker_config,
666 config_provider: self.config_provider,
667 })
668 }
669
670 pub fn mls_storage<NewS>(self, mls_storage: NewS) -> ClientBuilder<ApiClient, NewS, Db> {
671 ClientBuilder {
672 store: self.store,
673 api_client: self.api_client,
674 identity: self.identity,
675 identity_strategy: self.identity_strategy,
676 scw_verifier: self.scw_verifier,
677 custom_scw_verifier: self.custom_scw_verifier,
678 device_sync_worker_mode: self.device_sync_worker_mode,
679 fork_recovery_opts: self.fork_recovery_opts,
680 change_callbacks: self.change_callbacks,
681 stream_policy: self.stream_policy,
682 incoming_factory: self.incoming_factory,
683 version_info: self.version_info,
684 allow_offline: self.allow_offline,
685 disable_commit_log_worker: self.disable_commit_log_worker,
686 mls_storage: Some(mls_storage),
687 disable_workers: self.disable_workers,
688 worker_config: self.worker_config,
689 config_provider: self.config_provider,
690 }
691 }
692
693 pub fn with_disable_workers(mut self, disable_workers: bool) -> Self {
694 self.disable_workers = disable_workers;
695 self
696 }
697
698 pub fn with_device_sync_worker_mode(self, mode: Option<DeviceSyncMode>) -> Self {
699 Self {
700 device_sync_worker_mode: mode.unwrap_or(DeviceSyncMode::Enabled),
701 ..self
702 }
703 }
704
705 pub fn device_sync_worker_mode(self, mode: DeviceSyncMode) -> Self {
706 Self {
707 device_sync_worker_mode: mode,
708 ..self
709 }
710 }
711
712 pub fn fork_recovery_opts(self, opts: ForkRecoveryOpts) -> Self {
713 Self {
714 fork_recovery_opts: Some(opts),
715 ..self
716 }
717 }
718
719 pub fn worker_config(mut self, cfg: crate::worker::WorkerConfig) -> Self {
722 self.worker_config = cfg;
723 self
724 }
725
726 pub fn config_provider(
732 mut self,
733 provider: Arc<dyn xmtp_configuration::ConfigProvider>,
734 ) -> Self {
735 self.config_provider = Some(provider);
736 self
737 }
738
739 pub fn api_client<A>(self, api_client: A) -> ClientBuilder<A, S, Db> {
742 ClientBuilder {
743 api_client: Some(api_client),
744 identity: self.identity,
745 identity_strategy: self.identity_strategy,
746 scw_verifier: self.scw_verifier,
747 custom_scw_verifier: self.custom_scw_verifier,
748 store: self.store,
749 device_sync_worker_mode: self.device_sync_worker_mode,
750 fork_recovery_opts: self.fork_recovery_opts,
751 change_callbacks: self.change_callbacks,
752 stream_policy: self.stream_policy,
753 incoming_factory: None,
754 version_info: self.version_info,
755 allow_offline: self.allow_offline,
756 disable_commit_log_worker: self.disable_commit_log_worker,
757 mls_storage: self.mls_storage,
758 disable_workers: self.disable_workers,
759 worker_config: self.worker_config,
760 config_provider: self.config_provider,
761 }
762 }
763
764 xmtp_common::if_native! {
765 pub fn api_client_with_streams<A>(self, api_client: A) -> ClientBuilder<A, S, Db>
768 where
769 A: xmtp_proto::api_client::XmtpMlsBidiStreams
770 + crate::subscriptions::router_callbacks::ApiClientIdentity
771 + Clone
772 + Send
773 + Sync
774 + 'static,
775 A::SubscribeStream: 'static,
776 {
777 let factory = crate::subscriptions::incoming::BidiSubscriptionFactory {
778 api: api_client.clone(),
779 };
780 let mut builder = self.api_client(api_client);
781 builder.incoming_factory = Some(Arc::new(factory));
782 builder
783 }
784 }
785
786 xmtp_common::if_wasm! {
787 pub fn api_client_with_streams<A>(self, api_client: A) -> ClientBuilder<A, S, Db>
790 where
791 A: xmtp_proto::api_client::XmtpMlsStreams + Clone + 'static,
792 {
793 let api = api_client.clone();
794 let mut builder = self.api_client(api_client);
795 builder.incoming_factory = Some(Arc::new(move |cursors: xmtp_proto::types::TopicCursor, limits| -> crate::subscriptions::incoming::SubscriptionFuture {
796 let api = api.clone();
797 Box::pin(async move {
798 api.subscribe_envelopes_with_cursors(&cursors, limits)
799 .await
800 .map(|subscription| subscription.map_error(xmtp_proto::api::NetworkError::new))
801 .map_err(xmtp_proto::api::NetworkError::new)
802 })
803 }));
804 builder
805 }
806 }
807
808 pub fn maybe_version(
809 mut self,
810 version: Option<VersionInfo>,
811 ) -> ClientBuilder<ApiClient, S, Db> {
812 if let Some(v) = version {
813 self.version_info = v;
814 }
815 self
816 }
817
818 pub fn version(self, version_info: VersionInfo) -> ClientBuilder<ApiClient, S, Db> {
819 Self {
820 version_info,
821 ..self
822 }
823 }
824
825 pub fn with_allow_offline(
827 self,
828 allow_offline: Option<bool>,
829 ) -> ClientBuilder<ApiClient, S, Db> {
830 Self {
831 allow_offline: allow_offline.unwrap_or(false),
832 ..self
833 }
834 }
835
836 #[cfg(any(test, feature = "test-utils"))]
839 pub fn with_commit_log_worker(self, enabled: bool) -> Self {
840 Self {
841 disable_commit_log_worker: !enabled,
842 ..self
843 }
844 }
845
846 #[cfg(any(test, feature = "test-utils"))]
847 pub fn enable_sqlite_triggers(self) -> Self
848 where
849 Db: XmtpDb,
850 {
851 let db = self.store.as_ref().expect("unwrapping in test env").conn();
852 let db = xmtp_db::DbConnection::new(db);
853 db.register_triggers();
854 db.disable_memory_security();
855 self
856 }
857
858 pub fn enable_api_stats(
859 self,
860 ) -> Result<ClientBuilder<TrackedStatsClient<ApiClient>, S, Db>, ClientBuilderError> {
861 if self.api_client.is_none() {
862 return Err(ClientBuilderError::MissingParameter {
863 parameter: "api_client",
864 });
865 }
866
867 Ok(ClientBuilder {
868 api_client: Some(TrackedStatsClient::new(
869 self.api_client.expect("checked for none"),
870 )),
871 identity: self.identity,
872 identity_strategy: self.identity_strategy,
873 scw_verifier: self.scw_verifier,
874 custom_scw_verifier: self.custom_scw_verifier,
875 store: self.store,
876
877 device_sync_worker_mode: self.device_sync_worker_mode,
878 fork_recovery_opts: self.fork_recovery_opts,
879 change_callbacks: self.change_callbacks,
880 stream_policy: self.stream_policy,
881 incoming_factory: self.incoming_factory,
882 version_info: self.version_info,
883 allow_offline: self.allow_offline,
884 disable_commit_log_worker: self.disable_commit_log_worker,
885 mls_storage: self.mls_storage,
886 disable_workers: self.disable_workers,
887 worker_config: self.worker_config,
888 config_provider: self.config_provider,
889 })
890 }
891
892 pub fn with_scw_verifier(
893 self,
894 verifier: impl SmartContractSignatureVerifier + 'static,
895 ) -> ClientBuilder<ApiClient, S, Db> {
896 ClientBuilder {
897 api_client: self.api_client,
898 identity: self.identity,
899 identity_strategy: self.identity_strategy,
900 scw_verifier: Some(Box::new(verifier)),
901 custom_scw_verifier: true,
902 store: self.store,
903
904 device_sync_worker_mode: self.device_sync_worker_mode,
905 fork_recovery_opts: self.fork_recovery_opts,
906 change_callbacks: self.change_callbacks,
907 stream_policy: self.stream_policy,
908 incoming_factory: self.incoming_factory,
909 version_info: self.version_info,
910 allow_offline: self.allow_offline,
911 disable_commit_log_worker: self.disable_commit_log_worker,
912 mls_storage: self.mls_storage,
913 disable_workers: self.disable_workers,
914 worker_config: self.worker_config,
915 config_provider: self.config_provider,
916 }
917 }
918
919 pub fn with_remote_verifier(self) -> Result<ClientBuilder<ApiClient, S, Db>, ClientBuilderError>
922 where
923 ApiClient: Clone + XmtpApi + 'static,
924 {
925 let api = self
926 .api_client
927 .clone()
928 .ok_or(ClientBuilderError::MissingParameter {
929 parameter: "api_client",
930 })?;
931
932 Ok(ClientBuilder {
933 api_client: self.api_client,
934 identity: self.identity,
935 identity_strategy: self.identity_strategy,
936 scw_verifier: Some(Box::new(ApiClientWrapper::new(api, Retry::default()))
937 as Box<dyn SmartContractSignatureVerifier>),
938 custom_scw_verifier: false,
941 store: self.store,
942 device_sync_worker_mode: self.device_sync_worker_mode,
943 fork_recovery_opts: self.fork_recovery_opts,
944 change_callbacks: self.change_callbacks,
945 stream_policy: self.stream_policy,
946 incoming_factory: self.incoming_factory,
947 version_info: self.version_info,
948 allow_offline: self.allow_offline,
949 disable_commit_log_worker: self.disable_commit_log_worker,
950 mls_storage: self.mls_storage,
951 disable_workers: self.disable_workers,
952 worker_config: self.worker_config,
953 config_provider: self.config_provider,
954 })
955 }
956}
957
958#[cfg(test)]
959mod worker_registration_tests {
960 use crate::tester;
961 use crate::worker::{WorkerConfig, WorkerKind};
962
963 #[xmtp_common::test(unwrap_try = true)]
964 #[cfg_attr(target_arch = "wasm32", ignore)]
965 async fn disabled_worker_is_not_registered() {
966 let mut cfg = WorkerConfig::default();
967 cfg.enabled.insert(WorkerKind::DisappearingMessages, false);
968 tester!(alix, worker_config: cfg);
969
970 let kinds = alix.client.workers.registered_kinds();
971 assert!(
972 !kinds.contains(&WorkerKind::DisappearingMessages),
973 "disabled worker must not be registered, got {kinds:?}"
974 );
975 assert!(
976 kinds.contains(&WorkerKind::TaskRunner),
977 "un-disabled worker must still be registered, got {kinds:?}"
978 );
979 }
980}