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 };
166 Ok(ident)
167 }
168
169 pub fn inbox_id(&self, nonce: u64) -> Result<String, AssociationError> {
172 if !self.is_valid_address() {
173 return Err(AssociationError::InvalidAccountAddress);
174 }
175 let ident: MemberIdentifier = self.clone().into();
176 Ok(sha256_string(format!("{ident}{nonce}")))
177 }
178
179 fn is_valid_address(&self) -> bool {
182 match self {
183 Self::Ethereum(ident::Ethereum(addr)) => {
184 addr.len() == 42
185 && addr.starts_with("0x")
186 && addr[2..].chars().all(|c| c.is_ascii_hexdigit())
187 }
188 _ => true,
189 }
190 }
191}
192
193#[derive(Clone, Debug, PartialEq)]
194pub enum MemberKind {
195 Installation,
196 Ethereum,
197 Passkey,
198}
199
200impl Display for MemberKind {
201 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
202 match self {
203 MemberKind::Installation => write!(f, "installation"),
204 MemberKind::Ethereum => write!(f, "ethereum"),
205 MemberKind::Passkey => write!(f, "passkey"),
206 }
207 }
208}
209
210pub trait HasMemberKind {
211 fn kind(&self) -> MemberKind;
212}
213
214impl HasMemberKind for MemberIdentifier {
215 fn kind(&self) -> MemberKind {
216 match self {
217 Self::Installation(_) => MemberKind::Installation,
218 Self::Ethereum(_) => MemberKind::Ethereum,
219 Self::Passkey(_) => MemberKind::Passkey,
220 }
221 }
222}
223impl HasMemberKind for Identifier {
224 fn kind(&self) -> MemberKind {
225 match self {
226 Self::Ethereum(_) => MemberKind::Ethereum,
227 Self::Passkey(_) => MemberKind::Passkey,
228 }
229 }
230}
231
232impl Display for MemberIdentifier {
233 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
234 match self {
235 Self::Ethereum(eth) => write!(f, "{eth}"),
236 Self::Installation(ident) => write!(f, "{ident}"),
237 Self::Passkey(passkey) => write!(f, "{passkey}"),
238 }
239 }
240}
241
242impl Debug for MemberIdentifier {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match self {
245 Self::Installation(ident::Installation(key)) => f
246 .debug_tuple("Installation")
247 .field(&hex::encode(key))
248 .finish(),
249 Self::Ethereum(ident::Ethereum(addr)) => f.debug_tuple("Address").field(addr).finish(),
250 Self::Passkey(ident::Passkey { key, .. }) => {
251 f.debug_tuple("Passkey").field(&hex::encode(key)).finish()
252 }
253 }
254 }
255}
256
257impl Display for Identifier {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 match self {
260 Self::Ethereum(eth) => write!(f, "{eth}"),
261 Self::Passkey(passkey) => write!(f, "{passkey}"),
262 }
263 }
264}
265
266impl From<VerifyingKey> for MemberIdentifier {
267 fn from(installation: VerifyingKey) -> Self {
268 Self::Installation(ident::Installation(installation.as_bytes().to_vec()))
269 }
270}
271
272impl<'a> From<&'a XmtpInstallationCredential> for MemberIdentifier {
273 fn from(cred: &'a XmtpInstallationCredential) -> MemberIdentifier {
274 MemberIdentifier::Installation(ident::Installation(cred.public_slice().to_vec()))
275 }
276}
277
278impl From<XmtpInstallationCredential> for MemberIdentifier {
279 fn from(cred: XmtpInstallationCredential) -> MemberIdentifier {
280 MemberIdentifier::Installation(ident::Installation(cred.public_slice().to_vec()))
281 }
282}
283
284impl From<Identifier> for MemberIdentifier {
285 fn from(ident: Identifier) -> Self {
286 match ident {
287 Identifier::Ethereum(addr) => Self::Ethereum(addr),
288 Identifier::Passkey(passkey) => Self::Passkey(passkey),
289 }
290 }
291}
292impl From<MemberIdentifier> for Option<Identifier> {
293 fn from(ident: MemberIdentifier) -> Self {
294 let ident = match ident {
295 MemberIdentifier::Passkey(passkey) => Identifier::Passkey(passkey),
296 MemberIdentifier::Ethereum(eth) => Identifier::Ethereum(eth),
297 _ => {
298 return None;
299 }
300 };
301 Some(ident)
302 }
303}
304impl From<&Identifier> for GetInboxIdsRequestProto {
305 fn from(ident: &Identifier) -> Self {
306 Self {
307 identifier: format!("{ident}"),
308 identifier_kind: {
309 let kind: IdentifierKind = ident.into();
310 kind as i32
311 },
312 }
313 }
314}
315
316impl From<&Identifier> for ApiIdentifier {
317 fn from(ident: &Identifier) -> Self {
318 Self {
319 identifier: format!("{ident}"),
320 identifier_kind: ident.into(),
321 }
322 }
323}
324impl From<Identifier> for ApiIdentifier {
325 fn from(ident: Identifier) -> Self {
326 (&ident).into()
327 }
328}
329impl TryFrom<ApiIdentifier> for Identifier {
330 type Error = DeserializationError;
331 fn try_from(ident: ApiIdentifier) -> Result<Self, Self::Error> {
332 let ident = match ident.identifier_kind {
333 IdentifierKind::Unspecified | IdentifierKind::Ethereum => {
334 Identifier::eth(ident.identifier)?
335 }
336 IdentifierKind::Passkey => Identifier::Passkey(ident::Passkey {
337 key: hex::decode(ident.identifier)
338 .map_err(|_| DeserializationError::InvalidPasskey)?,
339 relying_party: None,
340 }),
341 };
342 Ok(ident)
343 }
344}
345
346#[derive(Clone, PartialEq)]
348pub struct Member {
349 pub identifier: MemberIdentifier,
350 pub added_by_entity: Option<MemberIdentifier>,
351 pub client_timestamp_ns: Option<u64>,
352 pub added_on_chain_id: Option<u64>,
353}
354
355impl std::fmt::Debug for Member {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 write!(
358 f,
359 "Member {{ identifier: {:?}, added_by {:?}, client_timestamp: {:?}, added_on_chain: {:?} }}",
360 self.identifier, self.added_by_entity, self.client_timestamp_ns, self.added_on_chain_id
361 )
362 }
363}
364
365impl Member {
366 pub fn new(
367 identifier: MemberIdentifier,
368 added_by_entity: Option<MemberIdentifier>,
369 client_timestamp_ns: Option<u64>,
370 added_on_chain_id: Option<u64>,
371 ) -> Self {
372 Self {
373 identifier,
374 added_by_entity,
375 client_timestamp_ns,
376 added_on_chain_id,
377 }
378 }
379
380 pub fn kind(&self) -> MemberKind {
381 self.identifier.kind()
382 }
383}
384
385impl PartialEq<MemberIdentifier> for Member {
386 fn eq(&self, other: &MemberIdentifier) -> bool {
387 self.identifier.eq(other)
388 }
389}
390impl PartialEq<MemberIdentifier> for Identifier {
391 fn eq(&self, other: &MemberIdentifier) -> bool {
392 match (self, other) {
393 (Self::Ethereum(ident), MemberIdentifier::Ethereum(other_ident)) => {
394 ident == other_ident
395 }
396 (Self::Passkey(ident), MemberIdentifier::Passkey(other_ident)) => ident == other_ident,
397 _ => false,
398 }
399 }
400}
401impl PartialEq<Identifier> for MemberIdentifier {
402 fn eq(&self, other: &Identifier) -> bool {
403 other == self
404 }
405}
406
407fn sha256_string(input: String) -> String {
409 let mut hasher = Sha256::new();
410 hasher.update(input.as_bytes());
411 format!("{:x}", hasher.finalize())
412}
413
414#[cfg(test)]
415pub(crate) mod tests {
416 use super::*;
417
418 #[allow(clippy::derivable_impls)]
419 impl Default for Member {
420 fn default() -> Self {
421 Self {
422 identifier: MemberIdentifier::rand_ethereum(),
423 added_by_entity: None,
424 client_timestamp_ns: None,
425 added_on_chain_id: None,
426 }
427 }
428 }
429
430 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
431 #[cfg_attr(not(target_arch = "wasm32"), test)]
432 fn test_identifier_comparisons() {
433 let address_1 = MemberIdentifier::rand_ethereum();
434 let address_2 = MemberIdentifier::rand_ethereum();
435 let address_1_copy = address_1.clone();
436
437 assert!(address_1 != address_2);
438 assert!(address_1.ne(&address_2));
439 assert!(address_1 == address_1_copy);
440
441 let installation_1 = MemberIdentifier::installation([1, 2, 3].to_vec());
442 let installation_2 = MemberIdentifier::installation([4, 5, 6].to_vec());
443 let installation_1_copy = MemberIdentifier::installation([1, 2, 3].to_vec());
444
445 assert!(installation_1 != installation_2);
446 assert!(installation_1.ne(&installation_2));
447 assert!(installation_1 == installation_1_copy);
448 }
449}