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