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}