1use std::sync::Arc;
10
11use parking_lot::RwLock;
12use prost::Message;
13use xmtp_api::{ApiClientWrapper, ApiError};
14use xmtp_configuration::{
15 ConfigProvider, ServerConfiguration, ServerConfigurationError, StoredConfigProvider,
16};
17use xmtp_db::prelude::*;
18use xmtp_db::{StorageError, server_configuration::StoredServerConfiguration};
19use xmtp_proto::api_client::XmtpBackendClient;
20use xmtp_proto::backend_v1;
21
22use crate::client::ClientError;
23
24#[derive(Debug, thiserror::Error)]
29pub enum ConfigurationFetchError {
30 #[error("the backend did not serve its configuration: {0}")]
33 Api(#[from] ApiError),
34 #[error("the fetched configuration could not be stored: {0}")]
36 Storage(#[from] StorageError),
37}
38
39impl xmtp_common::RetryableError for ConfigurationFetchError {
40 fn is_retryable(&self) -> bool {
41 match self {
42 Self::Api(e) => e.is_retryable(),
43 Self::Storage(e) => e.is_retryable(),
44 }
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
51pub enum ConfigurationLatch {
52 BackendMismatch { stored: String, received: String },
54 ClientVersionTooOld { client: String, minimum: String },
56}
57
58impl From<&ConfigurationLatch> for ClientError {
59 fn from(latch: &ConfigurationLatch) -> Self {
60 match latch {
61 ConfigurationLatch::BackendMismatch { stored, received } => {
62 ClientError::BackendMismatch {
63 stored: stored.clone(),
64 received: received.clone(),
65 }
66 }
67 ConfigurationLatch::ClientVersionTooOld { client, minimum } => {
68 ClientError::ClientVersionTooOld {
69 client: client.clone(),
70 minimum: minimum.clone(),
71 }
72 }
73 }
74 }
75}
76
77#[derive(Clone)]
82pub struct ServerConfigurationHandle {
83 provider: Arc<dyn ConfigProvider>,
84 latch: Arc<RwLock<Option<ConfigurationLatch>>>,
85 restricted_chains: Option<Arc<[String]>>,
89}
90
91impl std::fmt::Debug for ServerConfigurationHandle {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("ServerConfigurationHandle")
94 .field("identifier", &self.configuration().identifier)
95 .field("latch", &*self.latch.read())
96 .finish()
97 }
98}
99
100impl Default for ServerConfigurationHandle {
101 fn default() -> Self {
102 Self::new(Arc::new(StoredConfigProvider::default()))
103 }
104}
105
106impl ServerConfigurationHandle {
107 pub fn new(provider: Arc<dyn ConfigProvider>) -> Self {
117 let sanitized = {
118 let supplied = provider.server_configuration();
119 let limits = supplied.limits.without_zeroes();
120 (limits != supplied.limits).then(|| ServerConfiguration {
121 limits,
122 ..supplied.clone()
123 })
124 };
125 Self {
126 provider: match sanitized {
127 Some(configuration) => Arc::new(StoredConfigProvider::new(configuration)),
128 None => provider,
129 },
130 latch: Arc::default(),
131 restricted_chains: None,
132 }
133 }
134
135 pub(crate) fn with_chain_restriction(mut self, custom_verifier: bool) -> Self {
139 self.restricted_chains = (!custom_verifier).then(|| {
140 Arc::<[String]>::from(self.configuration().smart_contract_wallet_chains.clone())
141 });
142 self
143 }
144
145 pub fn restrict(&self, request: &mut xmtp_id::associations::builder::SignatureRequest) {
148 if let Some(chains) = self.restricted_chains.clone() {
149 request.restrict_chains(chains);
150 }
151 }
152
153 pub fn configuration(&self) -> &ServerConfiguration {
156 self.provider.server_configuration()
157 }
158
159 pub fn commit_log_enabled(&self) -> bool {
163 self.configuration().mls.commit_log_enabled()
164 }
165
166 pub fn latched(&self) -> Option<ConfigurationLatch> {
168 self.latch.read().clone()
169 }
170
171 pub fn check(&self) -> Result<(), ClientError> {
174 match self.latch.read().as_ref() {
175 Some(latch) => Err(latch.into()),
176 None => Ok(()),
177 }
178 }
179
180 pub(crate) fn latch(&self, latch: ConfigurationLatch) -> ClientError {
183 let mut guard = self.latch.write();
184 let held = guard.get_or_insert(latch);
185 ClientError::from(&*held)
186 }
187}
188
189fn snapshot_from(stored: &StoredServerConfiguration) -> ServerConfiguration {
195 match backend_v1::GetConfigurationResponse::decode(stored.response.as_slice()) {
196 Ok(response) => ServerConfiguration::from(response),
197 Err(error) => {
198 tracing::warn!(
199 identifier = %stored.identifier,
200 %error,
201 "stored server configuration does not decode; using compiled defaults"
202 );
203 ServerConfiguration {
204 identifier: stored.identifier.clone(),
205 ..Default::default()
206 }
207 }
208 }
209}
210
211fn validated(
213 response: &backend_v1::GetConfigurationResponse,
214) -> Result<ServerConfiguration, ServerConfigurationError> {
215 let configuration = ServerConfiguration::from(response.clone());
216 configuration.validate()?;
217 Ok(configuration)
218}
219
220pub(crate) async fn fetch_and_store<ApiClient>(
225 api: &ApiClientWrapper<ApiClient>,
226 db: &impl DbQuery,
227 handle: &ServerConfigurationHandle,
228) -> Result<ServerConfiguration, ClientError>
229where
230 ApiClient: XmtpBackendClient,
231{
232 handle.check()?;
233
234 let response = api.get_configuration().await.map_err(|error| {
235 ClientError::ConfigurationUnavailable(Box::new(ConfigurationFetchError::Api(error)))
236 })?;
237 let configuration = validated(&response)?;
238
239 let stored = db.server_configuration().map_err(storage_unavailable)?;
242 if let Some(stored) = stored.as_ref()
243 && !stored.identifier.is_empty()
244 && stored.identifier != configuration.identifier
245 {
246 tracing::error!(
247 stored = %stored.identifier,
248 received = %configuration.identifier,
249 "this database is bound to a different backend deployment"
250 );
251 if let Err(error) = db.record_server_configuration_conflict(&configuration.identifier) {
254 tracing::error!(%error, "could not record the conflicting backend identifier");
255 }
256 return Err(handle.latch(ConfigurationLatch::BackendMismatch {
257 stored: stored.identifier.clone(),
258 received: configuration.identifier.clone(),
259 }));
260 }
261
262 db.store_server_configuration(
263 &configuration.identifier,
264 normalized_url(api.backend_url().unwrap_or_default()),
265 &response.encode_to_vec(),
266 xmtp_common::time::now_ns(),
267 )
268 .map_err(storage_unavailable)?;
269
270 Ok(configuration)
271}
272
273pub async fn fetch_server_configuration<ApiClient>(
280 api: &ApiClientWrapper<ApiClient>,
281) -> Result<ServerConfiguration, ClientError>
282where
283 ApiClient: XmtpBackendClient,
284{
285 let response = api.get_configuration().await.map_err(|error| {
286 ClientError::ConfigurationUnavailable(Box::new(ConfigurationFetchError::Api(error)))
287 })?;
288 Ok(validated(&response)?)
289}
290
291fn normalized_url(url: &str) -> &str {
297 url.trim_end_matches('/')
298}
299
300fn storage_unavailable(error: StorageError) -> ClientError {
301 ClientError::ConfigurationUnavailable(Box::new(ConfigurationFetchError::Storage(error)))
302}
303
304pub(crate) async fn resolve<ApiClient>(
314 api: &ApiClientWrapper<ApiClient>,
315 db: &impl DbQuery,
316 allow_offline: bool,
317) -> Result<ServerConfigurationHandle, ClientError>
318where
319 ApiClient: XmtpBackendClient,
320{
321 let stored = db.server_configuration().map_err(storage_unavailable)?;
322
323 if let Some(conflicting) = stored
326 .as_ref()
327 .and_then(|row| row.conflicting_identifier.clone())
328 {
329 return Err(ClientError::BackendMismatch {
330 stored: stored.map(|row| row.identifier).unwrap_or_default(),
331 received: conflicting,
332 });
333 }
334
335 let configuration = match (allow_offline, stored) {
336 (true, None) => ServerConfiguration::default(),
339 (true, Some(stored)) => snapshot_from(&stored),
341 (false, None) => {
343 let handle = ServerConfigurationHandle::default();
344 fetch_and_store(api, db, &handle).await?
345 }
346 (false, Some(stored)) => {
347 let moved = api
351 .backend_url()
352 .is_some_and(|url| normalized_url(url) != normalized_url(&stored.backend_url));
353 if moved {
354 tracing::info!(
355 from = %stored.backend_url,
356 to = api.backend_url().unwrap_or_default(),
357 "backend URL changed; re-reading the deployment configuration"
358 );
359 let handle = ServerConfigurationHandle::default();
360 fetch_and_store(api, db, &handle).await?
361 } else {
362 snapshot_from(&stored)
363 }
364 }
365 };
366
367 Ok(ServerConfigurationHandle::new(Arc::new(
368 StoredConfigProvider::new(configuration),
369 )))
370}
371
372pub(crate) fn check_minimum_version(
375 configuration: &ServerConfiguration,
376 client_version: &semver::Version,
377) -> Result<(), ClientError> {
378 let Some(minimum) = configuration.minimum_version()? else {
379 return Ok(());
380 };
381 if xmtp_configuration::version_is_below(client_version, &minimum) {
382 return Err(ClientError::ClientVersionTooOld {
383 client: client_version.to_string(),
384 minimum: minimum.to_string(),
385 });
386 }
387 Ok(())
388}
389
390pub mod worker;
391
392#[cfg(test)]
393mod tests;