1use super::{AssociationError, DeserializationError, ident};
2use ed25519_dalek::VerifyingKey;
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::{
6 fmt::{Debug, Display},
7 hash::Hash,
8};
9use xmtp_cryptography::{XmtpInstallationCredential, signature::IdentifierValidationError};
10use xmtp_db::identity_cache::StoredIdentityKind;
11use xmtp_proto::types::ApiIdentifier;
12use xmtp_proto::{
13 ConversionError,
14 xmtp::identity::{
15 api::v1::get_inbox_ids_request::Request as GetInboxIdsRequestProto,
16 associations::IdentifierKind,
17 },
18};
19
20#[derive(Clone, Eq, PartialEq, Hash)]
21pub enum MemberIdentifier {
23 Installation(ident::Installation),
24 Ethereum(ident::Ethereum),
25 Passkey(ident::Passkey),
26}
27
28#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
29pub enum Identifier {
34 Ethereum(ident::Ethereum),
35 Passkey(ident::Passkey),
36}
37
38impl From<&Identifier> for StoredIdentityKind {
39 fn from(ident: &Identifier) -> Self {
40 match ident {
41 Identifier::Ethereum(_) => Self::Ethereum,
42 Identifier::Passkey(_) => Self::Passkey,
43 }
44 }
45}
46
47impl MemberIdentifier {
48 pub fn sanitize(self) -> Result<Self, IdentifierValidationError> {
49 let ident = match self {
50 Self::Ethereum(addr) => Self::Ethereum(addr.sanitize()?),
51 ident => ident,
52 };
53 Ok(ident)
54 }
55
56 #[cfg(any(test, feature = "test-utils"))]
57 pub fn rand_ethereum() -> Self {
58 Self::Ethereum(ident::Ethereum::rand())
59 }
60
61 #[cfg(any(test, feature = "test-utils"))]
62 pub fn rand_installation() -> Self {
63 Self::Installation(ident::Installation::rand())
64 }
65
66 pub fn eth(addr: impl ToString) -> Result<Self, IdentifierValidationError> {
67 Ok(Identifier::eth(addr)?.into())
68 }
69
70 pub fn installation(key: Vec<u8>) -> Self {
71 Self::Installation(ident::Installation(key))
72 }
73
74 pub fn installation_key(&self) -> Option<&[u8]> {
77 if let Self::Installation(installation) = self {
78 Some(&installation.0)
79 } else {
80 None
81 }
82 }
83
84 pub fn eth_address(&self) -> Option<&str> {
87 if let Self::Ethereum(address) = self {
88 Some(&address.0)
89 } else {
90 None
91 }
92 }
93
94 pub fn to_eth_address(self) -> Option<String> {
97 if let Self::Ethereum(address) = self {
98 Some(address.0)
99 } else {
100 None
101 }
102 }
103
104 pub fn to_installation(&self) -> Option<&[u8]> {
107 if let Self::Installation(installation) = self {
108 Some(&installation.0)
109 } else {
110 None
111 }
112 }
113}
114
115impl Identifier {
116 #[cfg(any(test, feature = "test-utils"))]
117 pub fn rand_ethereum() -> Self {
118 Self::Ethereum(ident::Ethereum::rand())
119 }
120
121 pub fn sanitize(self) -> Result<Self, IdentifierValidationError> {
122 let ident = match self {
123 Self::Ethereum(addr) => Self::Ethereum(addr.sanitize()?),
124 ident => ident,
125 };
126 Ok(ident)
127 }
128
129 pub fn eth(addr: impl ToString) -> Result<Self, IdentifierValidationError> {
130 Self::Ethereum(ident::Ethereum(addr.to_string())).sanitize()
131 }
132
133 pub fn passkey(key: Vec<u8>, relying_party: Option<String>) -> Self {
134 Self::Passkey(ident::Passkey { key, relying_party })
135 }
136
137 pub fn passkey_str(
138 key: &str,
139 relying_party: Option<String>,
140 ) -> Result<Self, IdentifierValidationError> {
141 Ok(Self::Passkey(ident::Passkey {
142 key: hex::decode(key)?,
143 relying_party,
144 }))
145 }
146
147 pub fn from_proto(
148 ident: impl AsRef<str>,
149 kind: IdentifierKind,
150 relying_party: Option<String>,
151 ) -> Result<Self, ConversionError> {
152 let ident = ident.as_ref();
153 let ident = match kind {
154 IdentifierKind::Unspecified | IdentifierKind::Ethereum => {
155 Self::Ethereum(ident::Ethereum(ident.to_string()))
156 }
157 IdentifierKind::Passkey => Self::Passkey(ident::Passkey {
158 key: hex::decode(ident).map_err(|_| ConversionError::InvalidPublicKey {
159 description: "passkey",
160 value: None,
161 })?,
162 relying_party,
163 }),
164 };
165 Ok(ident)
166 }
167
168 pub fn inbox_id(&self, nonce: u64) -> Result<String, AssociationError> {
171 if !self.is_valid_address() {
172 return Err(AssociationError::InvalidAccountAddress);
173 }
174 let ident: MemberIdentifier = self.clone().into();
175 Ok(sha256_string(format!("{ident}{nonce}")))
176 }
177
178 fn is_valid_address(&self) -> bool {
181 match self {
182 Self::Ethereum(ident::Ethereum(addr)) => {
183 addr.len() == 42
184 && addr.starts_with("0x")
185 && addr[2..].chars().all(|c| c.is_ascii_hexdigit())
186 }
187 _ => true,
188 }
189 }
190}
191
192#[derive(Clone, Debug, PartialEq)]
193pub enum MemberKind {
194 Installation,
195 Ethereum,
196 Passkey,
197}
198
199impl Display for MemberKind {
200 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
201 match self {
202 MemberKind::Installation => write!(f, "installation"),
203 MemberKind::Ethereum => write!(f, "ethereum"),
204 MemberKind::Passkey => write!(f, "passkey"),
205 }
206 }
207}
208
209pub trait HasMemberKind {
210 fn kind(&self) -> MemberKind;
211}
212
213impl HasMemberKind for MemberIdentifier {
214 fn kind(&self) -> MemberKind {
215 match self {
216 Self::Installation(_) => MemberKind::Installation,
217 Self::Ethereum(_) => MemberKind::Ethereum,
218 Self::Passkey(_) => MemberKind::Passkey,
219 }
220 }
221}
222impl HasMemberKind for Identifier {
223 fn kind(&self) -> MemberKind {
224 match self {
225 Self::Ethereum(_) => MemberKind::Ethereum,
226 Self::Passkey(_) => MemberKind::Passkey,
227 }
228 }
229}
230
231impl Display for MemberIdentifier {
232 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
233 match self {
234 Self::Ethereum(eth) => write!(f, "{eth}"),
235 Self::Installation(ident) => write!(f, "{ident}"),
236 Self::Passkey(passkey) => write!(f, "{passkey}"),
237 }
238 }
239}
240
241impl Debug for MemberIdentifier {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 match self {
244 Self::Installation(ident::Installation(key)) => f
245 .debug_tuple("Installation")
246 .field(&hex::encode(key))
247 .finish(),
248 Self::Ethereum(ident::Ethereum(addr)) => f.debug_tuple("Address").field(addr).finish(),
249 Self::Passkey(ident::Passkey { key, .. }) => {
250 f.debug_tuple("Passkey").field(&hex::encode(key)).finish()
251 }
252 }
253 }
254}
255
256impl Display for Identifier {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 match self {
259 Self::Ethereum(eth) => write!(f, "{eth}"),
260 Self::Passkey(passkey) => write!(f, "{passkey}"),
261 }
262 }
263}
264
265impl From<VerifyingKey> for MemberIdentifier {
266 fn from(installation: VerifyingKey) -> Self {
267 Self::Installation(ident::Installation(installation.as_bytes().to_vec()))
268 }
269}
270
271impl<'a> From<&'a XmtpInstallationCredential> for MemberIdentifier {
272 fn from(cred: &'a XmtpInstallationCredential) -> MemberIdentifier {
273 MemberIdentifier::Installation(ident::Installation(cred.public_slice().to_vec()))
274 }
275}
276
277impl From<XmtpInstallationCredential> for MemberIdentifier {
278 fn from(cred: XmtpInstallationCredential) -> MemberIdentifier {
279 MemberIdentifier::Installation(ident::Installation(cred.public_slice().to_vec()))
280 }
281}
282
283impl From<Identifier> for MemberIdentifier {
284 fn from(ident: Identifier) -> Self {
285 match ident {
286 Identifier::Ethereum(addr) => Self::Ethereum(addr),
287 Identifier::Passkey(passkey) => Self::Passkey(passkey),
288 }
289 }
290}
291impl From<MemberIdentifier> for Option<Identifier> {
292 fn from(ident: MemberIdentifier) -> Self {
293 let ident = match ident {
294 MemberIdentifier::Passkey(passkey) => Identifier::Passkey(passkey),
295 MemberIdentifier::Ethereum(eth) => Identifier::Ethereum(eth),
296 _ => {
297 return None;
298 }
299 };
300 Some(ident)
301 }
302}
303impl From<&Identifier> for GetInboxIdsRequestProto {
304 fn from(ident: &Identifier) -> Self {
305 Self {
306 identifier: format!("{ident}"),
307 identifier_kind: {
308 let kind: IdentifierKind = ident.into();
309 kind as i32
310 },
311 }
312 }
313}
314
315impl From<&Identifier> for ApiIdentifier {
316 fn from(ident: &Identifier) -> Self {
317 Self {
318 identifier: format!("{ident}"),
319 identifier_kind: ident.into(),
320 }
321 }
322}
323impl From<Identifier> for ApiIdentifier {
324 fn from(ident: Identifier) -> Self {
325 (&ident).into()
326 }
327}
328impl TryFrom<ApiIdentifier> for Identifier {
329 type Error = DeserializationError;
330 fn try_from(ident: ApiIdentifier) -> Result<Self, Self::Error> {
331 let ident = match ident.identifier_kind {
332 IdentifierKind::Unspecified | IdentifierKind::Ethereum => {
333 Identifier::eth(ident.identifier)?
334 }
335 IdentifierKind::Passkey => Identifier::Passkey(ident::Passkey {
336 key: hex::decode(ident.identifier)
337 .map_err(|_| DeserializationError::InvalidPasskey)?,
338 relying_party: None,
339 }),
340 };
341 Ok(ident)
342 }
343}
344
345#[derive(Clone, PartialEq)]
347pub struct Member {
348 pub identifier: MemberIdentifier,
349 pub added_by_entity: Option<MemberIdentifier>,
350 pub client_timestamp_ns: Option<u64>,
351 pub added_on_chain_id: Option<u64>,
352}
353
354impl std::fmt::Debug for Member {
355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 write!(
357 f,
358 "Member {{ identifier: {:?}, added_by {:?}, client_timestamp: {:?}, added_on_chain: {:?} }}",
359 self.identifier, self.added_by_entity, self.client_timestamp_ns, self.added_on_chain_id
360 )
361 }
362}
363
364impl Member {
365 pub fn new(
366 identifier: MemberIdentifier,
367 added_by_entity: Option<MemberIdentifier>,
368 client_timestamp_ns: Option<u64>,
369 added_on_chain_id: Option<u64>,
370 ) -> Self {
371 Self {
372 identifier,
373 added_by_entity,
374 client_timestamp_ns,
375 added_on_chain_id,
376 }
377 }
378
379 pub fn kind(&self) -> MemberKind {
380 self.identifier.kind()
381 }
382}
383
384impl PartialEq<MemberIdentifier> for Member {
385 fn eq(&self, other: &MemberIdentifier) -> bool {
386 self.identifier.eq(other)
387 }
388}
389impl PartialEq<MemberIdentifier> for Identifier {
390 fn eq(&self, other: &MemberIdentifier) -> bool {
391 match (self, other) {
392 (Self::Ethereum(ident), MemberIdentifier::Ethereum(other_ident)) => {
393 ident == other_ident
394 }
395 (Self::Passkey(ident), MemberIdentifier::Passkey(other_ident)) => ident == other_ident,
396 _ => false,
397 }
398 }
399}
400impl PartialEq<Identifier> for MemberIdentifier {
401 fn eq(&self, other: &Identifier) -> bool {
402 other == self
403 }
404}
405
406fn sha256_string(input: String) -> String {
408 let mut hasher = Sha256::new();
409 hasher.update(input.as_bytes());
410 format!("{:x}", hasher.finalize())
411}
412
413#[cfg(test)]
414pub(crate) mod tests {
415 #[cfg(target_arch = "wasm32")]
416 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);
417
418 use super::*;
419
420 #[allow(clippy::derivable_impls)]
421 impl Default for Member {
422 fn default() -> Self {
423 Self {
424 identifier: MemberIdentifier::rand_ethereum(),
425 added_by_entity: None,
426 client_timestamp_ns: None,
427 added_on_chain_id: None,
428 }
429 }
430 }
431
432 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
433 #[cfg_attr(not(target_arch = "wasm32"), test)]
434 fn test_identifier_comparisons() {
435 let address_1 = MemberIdentifier::rand_ethereum();
436 let address_2 = MemberIdentifier::rand_ethereum();
437 let address_1_copy = address_1.clone();
438
439 assert!(address_1 != address_2);
440 assert!(address_1.ne(&address_2));
441 assert!(address_1 == address_1_copy);
442
443 let installation_1 = MemberIdentifier::installation([1, 2, 3].to_vec());
444 let installation_2 = MemberIdentifier::installation([4, 5, 6].to_vec());
445 let installation_1_copy = MemberIdentifier::installation([1, 2, 3].to_vec());
446
447 assert!(installation_1 != installation_2);
448 assert!(installation_1.ne(&installation_2));
449 assert!(installation_1 == installation_1_copy);
450 }
451}