Skip to main content

xmtp_mls_identity/identity_lifecycle/
strategy.rs

1//! How a client finds or creates its identity on startup.
2
3use tracing::{debug, info};
4use xmtp_api::ApiClientWrapper;
5use xmtp_common::snippet::Snippet;
6use xmtp_db::identity::StoredIdentity;
7use xmtp_db::{Fetch, XmtpMlsStorageProvider};
8use xmtp_id::associations::Identifier;
9use xmtp_id::scw_verifier::SmartContractSignatureVerifier;
10use xmtp_id::{InboxId, InboxIdRef};
11use xmtp_mls_core::identity::{Identity, IdentityError};
12use xmtp_proto::api_client::XmtpApi;
13
14/**
15 * The identity strategy determines how the `xmtp_mls::builder::ClientBuilder` constructs an identity on startup.
16 *
17 * [`IdentityStrategy::CreateIfNotFound`] will attempt to create a new identity if one isn't found in the store.
18 * This is the default behavior.
19 *
20 * [`IdentityStrategy::CreateForIdentifier`] acts as `CreateIfNotFound` for the inbox of an identifier,
21 * which the builder asks the backend for only after it has checked the backend's deployment.
22 *
23 * [`IdentityStrategy::CachedOnly`] will attempt to get an identity from the store. If not found, it will
24 * return an error. This is useful if you don't want to create a new identity on startup because the caller
25 * does not have access to a signer.
26 *
27 * `IdentityStrategy::ExternalIdentity` allows you to provide an already-constructed identity to the
28 * client. This is useful for testing and not expected to be used in production.
29 */
30#[derive(Debug, Clone)]
31pub enum IdentityStrategy {
32    /// Tries to get an identity from the disk store. If not found, getting one from backend.
33    CreateIfNotFound {
34        inbox_id: InboxId,
35        identifier: Identifier,
36        nonce: u64,
37    },
38    /// `CreateIfNotFound` for the inbox the backend associates with the
39    /// identifier, or else the inbox its registration at the nonce creates.
40    CreateForIdentifier { identifier: Identifier, nonce: u64 },
41    /// Identity that is already in the disk store
42    CachedOnly,
43    /// An already-built Identity for testing purposes
44    #[cfg(any(test, feature = "test-utils"))]
45    ExternalIdentity(Identity),
46}
47
48impl IdentityStrategy {
49    pub fn inbox_id(&self) -> Option<InboxIdRef<'_>> {
50        use IdentityStrategy::*;
51        match self {
52            CreateIfNotFound { inbox_id, .. } => Some(inbox_id),
53            _ => None,
54        }
55    }
56
57    /// Create a new Identity Strategy, with [`IdentityStrategy::CreateIfNotFound`].
58    /// If an Identity is not found in the local store, creates a new one.
59    #[tracing::instrument(level = "trace", skip_all)]
60    pub fn new(inbox_id: InboxId, identifier: Identifier, nonce: u64) -> Self {
61        Self::CreateIfNotFound {
62            inbox_id,
63            identifier,
64            nonce,
65        }
66    }
67
68    /// Create an [`IdentityStrategy::CreateForIdentifier`], for a caller that
69    /// does not know the identifier's inbox. The build takes the inbox the
70    /// backend names, or the one the identifier creates at `nonce`, and fails
71    /// with [`IdentityError::IdentifierNotInInbox`] unless the identifier
72    /// belongs to it, as
73    /// `xmtp_mls::builder::ClientBuilder::build`
74    /// describes. An offline build checks the newest state the database
75    /// stores, which can predate a revocation.
76    pub fn for_identifier(identifier: Identifier, nonce: u64) -> Self {
77        Self::CreateForIdentifier { identifier, nonce }
78    }
79
80    /// Replace [`IdentityStrategy::CreateForIdentifier`] with
81    /// [`IdentityStrategy::CreateIfNotFound`] for the identifier's inbox. Call
82    /// it only after the backend's deployment is checked: the request carries
83    /// the identifier.
84    pub async fn with_inbox<ApiClient: XmtpApi>(
85        self,
86        api_client: &ApiClientWrapper<ApiClient>,
87    ) -> Result<Self, IdentityError> {
88        let Self::CreateForIdentifier { identifier, nonce } = self else {
89            return Ok(self);
90        };
91        let found = api_client
92            .get_inbox_ids(vec![identifier.clone().into()])
93            .await?;
94        let inbox_id = match found.into_iter().next().flatten() {
95            Some(inbox_id) => inbox_id,
96            None => identifier.inbox_id(nonce)?,
97        };
98        Ok(Self::new(inbox_id, identifier, nonce))
99    }
100}
101
102impl IdentityStrategy {
103    /**
104     * Initialize an identity from the given strategy. If a stored identity is found in the database,
105     * it will return that identity.
106     *
107     * If a stored identity is found, it will validate that the inbox_id of the stored identity matches
108     * the inbox_id configured on the strategy.
109     *
110     **/
111    #[tracing::instrument(level = "trace", skip_all)]
112    pub async fn initialize_identity<ApiClient: XmtpApi, S: XmtpMlsStorageProvider>(
113        self,
114        api_client: &ApiClientWrapper<ApiClient>,
115        mls_storage: &S,
116        scw_signature_verifier: impl SmartContractSignatureVerifier,
117    ) -> Result<Identity, IdentityError> {
118        use IdentityStrategy::*;
119
120        info!("Initializing identity");
121        let stored_identity: Option<Identity> = mls_storage
122            .db()
123            .fetch(&())?
124            .map(|i: StoredIdentity| i.try_into())
125            .transpose()?;
126
127        let strategy = match &self {
128            CreateIfNotFound { .. } => "create_if_not_found",
129            CreateForIdentifier { .. } => "create_for_identifier",
130            CachedOnly => "cached_only",
131            #[cfg(any(test, feature = "test-utils"))]
132            ExternalIdentity(_) => "external_identity",
133        };
134        debug!(
135            strategy,
136            stored_identity_present = stored_identity.is_some(),
137            "identity strategy"
138        );
139        match self.with_inbox(api_client).await? {
140            CachedOnly => stored_identity.ok_or(IdentityError::RequiredIdentityNotFound),
141            CreateForIdentifier { .. } => unreachable!("with_inbox replaces the strategy"),
142            CreateIfNotFound {
143                inbox_id,
144                identifier,
145                nonce,
146            } => {
147                if let Some(stored_identity) = stored_identity {
148                    tracing::debug!(
149                        installation_id =
150                            stored_identity.installation_keys.public_bytes().snippet(),
151                        inbox_id = stored_identity.inbox_id,
152                        "Found existing identity in store"
153                    );
154                    if inbox_id != stored_identity.inbox_id {
155                        return Err(IdentityError::InboxIdMismatch {
156                            id: inbox_id.clone(),
157                            stored: stored_identity.inbox_id,
158                        });
159                    }
160
161                    Ok(stored_identity)
162                } else {
163                    crate::identity_lifecycle::new_identity(
164                        inbox_id,
165                        identifier,
166                        nonce,
167                        api_client,
168                        mls_storage,
169                        &scw_signature_verifier,
170                    )
171                    .await
172                }
173            }
174            #[cfg(any(test, feature = "test-utils"))]
175            ExternalIdentity(identity) => Ok(identity),
176        }
177    }
178}