Skip to main content

xmtp_id/associations/
serialization.rs

1use super::{
2    MemberIdentifier, SignatureError, ident,
3    member::{Identifier, Member},
4    signature::{AccountId, ValidatedLegacySignedPublicKey},
5    state::{AssociationState, AssociationStateDiff},
6    unsigned_actions::{
7        UnsignedAddAssociation, UnsignedChangeRecoveryAddress, UnsignedCreateInbox,
8        UnsignedRevokeAssociation,
9    },
10    unverified::{
11        UnverifiedAction, UnverifiedAddAssociation, UnverifiedChangeRecoveryAddress,
12        UnverifiedCreateInbox, UnverifiedIdentityUpdate, UnverifiedInstallationKeySignature,
13        UnverifiedLegacyDelegatedSignature, UnverifiedRecoverableEcdsaSignature,
14        UnverifiedRevokeAssociation, UnverifiedSignature, UnverifiedSmartContractWalletSignature,
15    },
16    verified_signature::VerifiedSignature,
17};
18use crate::scw_verifier::ValidationResponse;
19use prost::{DecodeError, Message};
20use regex::Regex;
21use std::collections::{HashMap, HashSet};
22use thiserror::Error;
23use xmtp_common::ErrorCode;
24use xmtp_cryptography::signature::{IdentifierValidationError, sanitize_evm_addresses};
25use xmtp_proto::ConversionError;
26use xmtp_proto::xmtp::{
27    identity::{
28        api::v1::verify_smart_contract_wallet_signatures_response::ValidationResponse as SmartContractWalletValidationResponseProto,
29        associations::{
30            AddAssociation as AddAssociationProto, AssociationState as AssociationStateProto,
31            AssociationStateDiff as AssociationStateDiffProto,
32            ChangeRecoveryAddress as ChangeRecoveryAddressProto, CreateInbox as CreateInboxProto,
33            IdentifierKind, IdentityAction as IdentityActionProto,
34            IdentityUpdate as IdentityUpdateProto,
35            LegacyDelegatedSignature as LegacyDelegatedSignatureProto, Member as MemberProto,
36            MemberIdentifier as MemberIdentifierProto, MemberMap as MemberMapProto,
37            Passkey as PasskeyProto, RecoverableEcdsaSignature as RecoverableEcdsaSignatureProto,
38            RecoverableEd25519Signature as RecoverableEd25519SignatureProto,
39            RecoverablePasskeySignature as RecoverablePasskeySignatureProto,
40            RevokeAssociation as RevokeAssociationProto, Signature as SignatureWrapperProto,
41            SmartContractWalletSignature as SmartContractWalletSignatureProto,
42            identity_action::Kind as IdentityActionKindProto,
43            member_identifier::Kind as MemberIdentifierKindProto,
44            signature::Signature as SignatureKindProto,
45        },
46    },
47    message_contents::{
48        Signature as SignedPublicKeySignatureProto, SignedPublicKey as LegacySignedPublicKeyProto,
49        SignedPublicKey as SignedPublicKeyProto, UnsignedPublicKey as LegacyUnsignedPublicKeyProto,
50        signature::{Union, WalletEcdsaCompact},
51        unsigned_public_key,
52    },
53};
54
55#[derive(Error, Debug, ErrorCode)]
56pub enum DeserializationError {
57    #[error(transparent)]
58    #[error_code(inherit)]
59    SignatureError(#[from] crate::associations::SignatureError),
60    /// Missing action.
61    ///
62    /// Identity action not present in update. Not retryable.
63    #[error("Missing action")]
64    MissingAction,
65    /// Missing update.
66    ///
67    /// Identity update not present. Not retryable.
68    #[error("Missing update")]
69    MissingUpdate,
70    /// Missing member identifier.
71    ///
72    /// Member identifier field empty. Not retryable.
73    #[error("Missing member identifier")]
74    MissingMemberIdentifier,
75    /// Missing signature.
76    ///
77    /// Signature field not present. Not retryable.
78    #[error("Missing signature")]
79    Signature,
80    /// Missing member.
81    ///
82    /// Member field not present. Not retryable.
83    #[error("Missing Member")]
84    MissingMember,
85    /// Decode error.
86    ///
87    /// Protobuf decoding failed. Not retryable.
88    #[error("Decode error {0}")]
89    Decode(#[from] DecodeError),
90    /// Invalid account ID.
91    ///
92    /// CAIP-10 account ID is malformed. Not retryable.
93    #[error("Invalid account id")]
94    InvalidAccountId,
95    /// Invalid passkey.
96    ///
97    /// Passkey data is malformed. Not retryable.
98    #[error("Invalid passkey")]
99    InvalidPasskey,
100    /// Invalid hash.
101    ///
102    /// Hash must be 32 bytes. Not retryable.
103    #[error("Invalid hash (needs to be 32 bytes)")]
104    InvalidHash,
105    /// Unspecified value.
106    ///
107    /// An unrecognized or unsupported value was encountered. Not retryable.
108    #[error("A required field is unspecified: {0}")]
109    Unspecified(&'static str),
110    /// Deprecated field.
111    ///
112    /// A deprecated field was used. Not retryable.
113    #[error("Field is deprecated: {0}")]
114    Deprecated(&'static str),
115    /// Ed25519 key error.
116    ///
117    /// Failed to create public key from bytes. Not retryable.
118    #[error("Error creating public key from proto bytes")]
119    Ed25519(#[from] ed25519_dalek::ed25519::Error),
120    /// Unable to deserialize.
121    ///
122    /// Bincode deserialization failed. Not retryable.
123    #[error("Unable to deserialize")]
124    Bincode,
125    #[error(transparent)]
126    #[error_code(inherit)]
127    AddressValidation(#[from] IdentifierValidationError),
128}
129
130impl TryFrom<IdentityUpdateProto> for UnverifiedIdentityUpdate {
131    type Error = ConversionError;
132
133    fn try_from(proto: IdentityUpdateProto) -> Result<Self, Self::Error> {
134        let IdentityUpdateProto {
135            client_timestamp_ns,
136            inbox_id,
137            actions,
138        } = proto;
139        let all_actions = actions
140            .into_iter()
141            .map(|action| match action.kind {
142                Some(action) => Ok(action),
143                None => Err(ConversionError::Missing {
144                    item: "action",
145                    r#type: std::any::type_name::<IdentityActionKindProto>(),
146                }),
147            })
148            .collect::<Result<Vec<IdentityActionKindProto>, ConversionError>>()?;
149
150        let processed_actions: Vec<UnverifiedAction> = all_actions
151            .into_iter()
152            .map(UnverifiedAction::try_from)
153            .collect::<Result<Vec<UnverifiedAction>, ConversionError>>()?;
154
155        Ok(UnverifiedIdentityUpdate::new(
156            inbox_id,
157            client_timestamp_ns,
158            processed_actions,
159        ))
160    }
161}
162
163impl TryFrom<IdentityActionKindProto> for UnverifiedAction {
164    type Error = ConversionError;
165
166    fn try_from(action: IdentityActionKindProto) -> Result<Self, Self::Error> {
167        Ok(match action {
168            IdentityActionKindProto::Add(add_action) => {
169                UnverifiedAction::AddAssociation(UnverifiedAddAssociation {
170                    new_member_signature: add_action.new_member_signature.try_into()?,
171                    existing_member_signature: add_action.existing_member_signature.try_into()?,
172                    unsigned_action: UnsignedAddAssociation {
173                        new_member_identifier: add_action
174                            .new_member_identifier
175                            .ok_or(ConversionError::Missing {
176                                item: "member_identifier",
177                                r#type: std::any::type_name::<MemberIdentifierProto>(),
178                            })?
179                            .try_into()?,
180                    },
181                })
182            }
183            IdentityActionKindProto::CreateInbox(action_proto) => {
184                let kind = match action_proto.initial_identifier_kind() {
185                    IdentifierKind::Unspecified => IdentifierKind::Ethereum,
186                    kind => kind,
187                };
188                let account_identifier = Identifier::from_proto(
189                    &action_proto.initial_identifier,
190                    kind,
191                    action_proto.relying_party,
192                )?;
193
194                UnverifiedAction::CreateInbox(UnverifiedCreateInbox {
195                    initial_identifier_signature: action_proto
196                        .initial_identifier_signature
197                        .try_into()?,
198                    unsigned_action: UnsignedCreateInbox {
199                        nonce: action_proto.nonce,
200                        account_identifier,
201                    },
202                })
203            }
204            IdentityActionKindProto::ChangeRecoveryAddress(action_proto) => {
205                let kind = match action_proto.new_recovery_identifier_kind() {
206                    IdentifierKind::Unspecified => IdentifierKind::Ethereum,
207                    kind => kind,
208                };
209                let new_recovery_identifier = Identifier::from_proto(
210                    &action_proto.new_recovery_identifier,
211                    kind,
212                    action_proto.relying_party,
213                )?;
214                UnverifiedAction::ChangeRecoveryAddress(UnverifiedChangeRecoveryAddress {
215                    recovery_identifier_signature: action_proto
216                        .existing_recovery_identifier_signature
217                        .try_into()?,
218                    unsigned_action: UnsignedChangeRecoveryAddress {
219                        new_recovery_identifier,
220                    },
221                })
222            }
223            IdentityActionKindProto::Revoke(action_proto) => {
224                UnverifiedAction::RevokeAssociation(UnverifiedRevokeAssociation {
225                    recovery_identifier_signature: action_proto
226                        .recovery_identifier_signature
227                        .try_into()?,
228                    unsigned_action: UnsignedRevokeAssociation {
229                        revoked_member: action_proto
230                            .member_to_revoke
231                            .ok_or(ConversionError::Missing {
232                                item: "member_to_revoke",
233                                r#type: std::any::type_name::<MemberIdentifierProto>(),
234                            })?
235                            .try_into()?,
236                    },
237                })
238            }
239        })
240    }
241}
242
243impl TryFrom<SignatureWrapperProto> for UnverifiedSignature {
244    type Error = ConversionError;
245
246    fn try_from(proto: SignatureWrapperProto) -> Result<Self, Self::Error> {
247        let signature = unwrap_proto_signature(proto)?;
248        let unverified_sig = match signature {
249            SignatureKindProto::Erc191(sig) => UnverifiedSignature::RecoverableEcdsa(
250                UnverifiedRecoverableEcdsaSignature::new(sig.bytes),
251            ),
252            SignatureKindProto::DelegatedErc191(sig) => {
253                UnverifiedSignature::LegacyDelegated(UnverifiedLegacyDelegatedSignature::new(
254                    UnverifiedRecoverableEcdsaSignature::new(
255                        sig.signature
256                            .ok_or(ConversionError::Missing {
257                                item: "signature",
258                                r#type: std::any::type_name::<RecoverableEcdsaSignatureProto>(),
259                            })?
260                            .bytes,
261                    ),
262                    sig.delegated_key.ok_or(ConversionError::Missing {
263                        item: "delegated_key",
264                        r#type: std::any::type_name::<SignedPublicKeyProto>(),
265                    })?,
266                ))
267            }
268            SignatureKindProto::InstallationKey(sig) => {
269                UnverifiedSignature::InstallationKey(UnverifiedInstallationKeySignature::new(
270                    sig.bytes,
271                    sig.public_key.as_slice().try_into()?,
272                ))
273            }
274            SignatureKindProto::Erc6492(sig) => UnverifiedSignature::SmartContractWallet(
275                UnverifiedSmartContractWalletSignature::new(
276                    sig.signature,
277                    sig.account_id.try_into()?,
278                    sig.block_number,
279                ),
280            ),
281            SignatureKindProto::Passkey(sig) => UnverifiedSignature::new_passkey(
282                sig.public_key,
283                sig.signature,
284                sig.authenticator_data,
285                sig.client_data_json,
286            ),
287        };
288
289        Ok(unverified_sig)
290    }
291}
292
293impl TryFrom<Option<SignatureWrapperProto>> for UnverifiedSignature {
294    type Error = ConversionError;
295
296    fn try_from(value: Option<SignatureWrapperProto>) -> Result<Self, Self::Error> {
297        value
298            .ok_or_else(|| ConversionError::Missing {
299                item: "signature",
300                r#type: std::any::type_name::<SignatureWrapperProto>(),
301            })?
302            .try_into()
303    }
304}
305
306fn unwrap_proto_signature(
307    value: SignatureWrapperProto,
308) -> Result<SignatureKindProto, ConversionError> {
309    match value.signature {
310        Some(inner) => Ok(inner),
311        None => Err(ConversionError::Missing {
312            item: "signature",
313            r#type: std::any::type_name::<SignatureKindProto>(),
314        }),
315    }
316}
317
318impl From<UnverifiedIdentityUpdate> for IdentityUpdateProto {
319    fn from(value: UnverifiedIdentityUpdate) -> Self {
320        Self {
321            inbox_id: value.inbox_id,
322            client_timestamp_ns: value.client_timestamp_ns,
323            actions: map_vec(value.actions),
324        }
325    }
326}
327
328impl From<UnverifiedAction> for IdentityActionProto {
329    fn from(value: UnverifiedAction) -> Self {
330        let kind: IdentityActionKindProto = match value {
331            UnverifiedAction::CreateInbox(action) => {
332                let account_identifier = action.unsigned_action.account_identifier;
333                let initial_identifier = format!("{account_identifier}");
334                let relying_party = match &account_identifier {
335                    Identifier::Passkey(pk) => pk.relying_party.clone(),
336                    _ => None,
337                };
338                let initial_identifier_kind: IdentifierKind = account_identifier.into();
339                IdentityActionKindProto::CreateInbox(CreateInboxProto {
340                    nonce: action.unsigned_action.nonce,
341                    initial_identifier,
342                    initial_identifier_kind: initial_identifier_kind as i32,
343                    initial_identifier_signature: Some(action.initial_identifier_signature.into()),
344                    relying_party,
345                })
346            }
347            UnverifiedAction::AddAssociation(action) => {
348                let relying_party = match &action.unsigned_action.new_member_identifier {
349                    MemberIdentifier::Passkey(pk) => pk.relying_party.clone(),
350                    _ => None,
351                };
352                IdentityActionKindProto::Add(AddAssociationProto {
353                    new_member_identifier: Some(
354                        action.unsigned_action.new_member_identifier.into(),
355                    ),
356                    existing_member_signature: Some(action.existing_member_signature.into()),
357                    new_member_signature: Some(action.new_member_signature.into()),
358                    relying_party,
359                })
360            }
361            UnverifiedAction::ChangeRecoveryAddress(action) => {
362                let new_recovery_identifier = action.unsigned_action.new_recovery_identifier;
363                let new_recovery_identifier_string = format!("{new_recovery_identifier}");
364                let relying_party = match &new_recovery_identifier {
365                    Identifier::Passkey(pk) => pk.relying_party.clone(),
366                    _ => None,
367                };
368                let new_recovery_identifier_kind: IdentifierKind = new_recovery_identifier.into();
369                IdentityActionKindProto::ChangeRecoveryAddress(ChangeRecoveryAddressProto {
370                    new_recovery_identifier: new_recovery_identifier_string,
371                    new_recovery_identifier_kind: new_recovery_identifier_kind as i32,
372                    existing_recovery_identifier_signature: Some(
373                        action.recovery_identifier_signature.into(),
374                    ),
375                    relying_party,
376                })
377            }
378            UnverifiedAction::RevokeAssociation(action) => {
379                IdentityActionKindProto::Revoke(RevokeAssociationProto {
380                    recovery_identifier_signature: Some(
381                        action.recovery_identifier_signature.into(),
382                    ),
383                    member_to_revoke: Some(action.unsigned_action.revoked_member.into()),
384                })
385            }
386        };
387
388        IdentityActionProto { kind: Some(kind) }
389    }
390}
391
392impl From<&Identifier> for IdentifierKind {
393    fn from(ident: &Identifier) -> Self {
394        match ident {
395            Identifier::Ethereum(_) => IdentifierKind::Ethereum,
396            Identifier::Passkey(_) => IdentifierKind::Passkey,
397        }
398    }
399}
400impl From<Identifier> for IdentifierKind {
401    fn from(ident: Identifier) -> Self {
402        (&ident).into()
403    }
404}
405
406impl From<UnverifiedSignature> for SignatureWrapperProto {
407    fn from(value: UnverifiedSignature) -> Self {
408        let signature = match value {
409            UnverifiedSignature::SmartContractWallet(sig) => {
410                SignatureKindProto::Erc6492(SmartContractWalletSignatureProto {
411                    account_id: sig.account_id.into(),
412                    block_number: sig.block_number,
413                    signature: sig.signature_bytes,
414                })
415            }
416            UnverifiedSignature::InstallationKey(UnverifiedInstallationKeySignature {
417                signature_bytes,
418                verifying_key,
419            }) => SignatureKindProto::InstallationKey(RecoverableEd25519SignatureProto {
420                bytes: signature_bytes,
421                public_key: verifying_key.as_bytes().to_vec(),
422            }),
423            UnverifiedSignature::LegacyDelegated(sig) => {
424                SignatureKindProto::DelegatedErc191(LegacyDelegatedSignatureProto {
425                    delegated_key: Some(sig.signed_public_key_proto),
426                    signature: Some(RecoverableEcdsaSignatureProto {
427                        bytes: sig.legacy_key_signature.signature_bytes,
428                    }),
429                })
430            }
431            UnverifiedSignature::RecoverableEcdsa(sig) => {
432                SignatureKindProto::Erc191(RecoverableEcdsaSignatureProto {
433                    bytes: sig.signature_bytes,
434                })
435            }
436            UnverifiedSignature::Passkey(sig) => {
437                SignatureKindProto::Passkey(RecoverablePasskeySignatureProto {
438                    public_key: sig.public_key,
439                    signature: sig.signature,
440                    authenticator_data: sig.authenticator_data,
441                    client_data_json: sig.client_data_json,
442                })
443            }
444        };
445
446        Self {
447            signature: Some(signature),
448        }
449    }
450}
451
452impl TryFrom<Vec<u8>> for UnverifiedIdentityUpdate {
453    type Error = ConversionError;
454
455    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
456        let update_proto: IdentityUpdateProto = IdentityUpdateProto::decode(value.as_slice())?;
457        UnverifiedIdentityUpdate::try_from(update_proto)
458    }
459}
460
461impl From<UnverifiedIdentityUpdate> for Vec<u8> {
462    fn from(value: UnverifiedIdentityUpdate) -> Self {
463        let proto: IdentityUpdateProto = value.into();
464        proto.encode_to_vec()
465    }
466}
467
468impl From<SmartContractWalletValidationResponseProto> for ValidationResponse {
469    fn from(value: SmartContractWalletValidationResponseProto) -> Self {
470        Self {
471            is_valid: value.is_valid,
472            block_number: value.block_number,
473            error: value.error,
474        }
475    }
476}
477
478impl From<MemberIdentifierKindProto> for MemberIdentifier {
479    fn from(proto: MemberIdentifierKindProto) -> Self {
480        match proto {
481            MemberIdentifierKindProto::EthereumAddress(address) => {
482                Self::Ethereum(ident::Ethereum(address))
483            }
484            MemberIdentifierKindProto::InstallationPublicKey(public_key) => {
485                Self::Installation(ident::Installation(public_key))
486            }
487            MemberIdentifierKindProto::Passkey(PasskeyProto { key, relying_party }) => {
488                Self::Passkey(ident::Passkey { key, relying_party })
489            }
490        }
491    }
492}
493
494impl From<Member> for MemberProto {
495    fn from(member: Member) -> MemberProto {
496        MemberProto {
497            identifier: Some(member.identifier.into()),
498            added_by_entity: member.added_by_entity.map(Into::into),
499            client_timestamp_ns: member.client_timestamp_ns,
500            added_on_chain_id: member.added_on_chain_id,
501        }
502    }
503}
504
505impl TryFrom<MemberProto> for Member {
506    type Error = ConversionError;
507
508    fn try_from(proto: MemberProto) -> Result<Self, Self::Error> {
509        Ok(Member {
510            identifier: proto
511                .identifier
512                .ok_or(ConversionError::Missing {
513                    item: "member_identifier",
514                    r#type: std::any::type_name::<MemberIdentifierProto>(),
515                })?
516                .try_into()?,
517            added_by_entity: proto.added_by_entity.map(TryInto::try_into).transpose()?,
518            client_timestamp_ns: proto.client_timestamp_ns,
519            added_on_chain_id: proto.added_on_chain_id,
520        })
521    }
522}
523
524impl From<MemberIdentifier> for MemberIdentifierProto {
525    fn from(member_identifier: MemberIdentifier) -> MemberIdentifierProto {
526        match member_identifier {
527            MemberIdentifier::Ethereum(ident::Ethereum(address)) => MemberIdentifierProto {
528                kind: Some(MemberIdentifierKindProto::EthereumAddress(address)),
529            },
530            MemberIdentifier::Installation(ident::Installation(public_key)) => {
531                MemberIdentifierProto {
532                    kind: Some(MemberIdentifierKindProto::InstallationPublicKey(public_key)),
533                }
534            }
535            MemberIdentifier::Passkey(ident::Passkey { key, relying_party }) => {
536                MemberIdentifierProto {
537                    kind: Some(MemberIdentifierKindProto::Passkey(PasskeyProto {
538                        key,
539                        relying_party,
540                    })),
541                }
542            }
543        }
544    }
545}
546
547impl TryFrom<MemberIdentifierProto> for MemberIdentifier {
548    type Error = ConversionError;
549
550    fn try_from(proto: MemberIdentifierProto) -> Result<Self, Self::Error> {
551        match proto.kind {
552            Some(MemberIdentifierKindProto::EthereumAddress(address)) => {
553                Ok(MemberIdentifier::Ethereum(ident::Ethereum(address)))
554            }
555            Some(MemberIdentifierKindProto::InstallationPublicKey(public_key)) => Ok(
556                MemberIdentifier::Installation(ident::Installation(public_key)),
557            ),
558            Some(MemberIdentifierKindProto::Passkey(PasskeyProto { key, relying_party })) => {
559                Ok(MemberIdentifier::Passkey(ident::Passkey {
560                    key,
561                    relying_party,
562                }))
563            }
564            None => Err(ConversionError::Missing {
565                item: "member_identifier",
566                r#type: std::any::type_name::<MemberIdentifierKindProto>(),
567            }),
568        }
569    }
570}
571
572impl From<AssociationState> for AssociationStateProto {
573    fn from(state: AssociationState) -> AssociationStateProto {
574        let members = state
575            .members
576            .into_iter()
577            .map(|(key, value)| MemberMapProto {
578                key: Some(key.into()),
579                value: Some(value.into()),
580            })
581            .collect();
582
583        let kind: IdentifierKind = (&state.recovery_identifier).into();
584        let relying_party = match &state.recovery_identifier {
585            Identifier::Passkey(ident::Passkey { relying_party, .. }) => relying_party.clone(),
586            _ => None,
587        };
588
589        AssociationStateProto {
590            inbox_id: state.inbox_id,
591            members,
592            recovery_identifier: state.recovery_identifier.to_string(),
593            recovery_identifier_kind: kind as i32,
594            seen_signatures: state.seen_signatures.into_iter().collect(),
595            relying_party,
596        }
597    }
598}
599
600impl TryFrom<AssociationStateProto> for AssociationState {
601    type Error = ConversionError;
602
603    fn try_from(proto: AssociationStateProto) -> Result<Self, Self::Error> {
604        let kind = match proto.recovery_identifier_kind() {
605            IdentifierKind::Unspecified => IdentifierKind::Ethereum,
606            kind => kind,
607        };
608        let recovery_identifier =
609            Identifier::from_proto(&proto.recovery_identifier, kind, proto.relying_party)?;
610
611        let members = proto
612            .members
613            .into_iter()
614            .map(|kv| {
615                let key = kv
616                    .key
617                    .ok_or(ConversionError::Missing {
618                        item: "member_identifier",
619                        r#type: std::any::type_name::<MemberIdentifierProto>(),
620                    })?
621                    .try_into()?;
622                let value = kv
623                    .value
624                    .ok_or(ConversionError::Missing {
625                        item: "member",
626                        r#type: std::any::type_name::<MemberProto>(),
627                    })?
628                    .try_into()?;
629                Ok((key, value))
630            })
631            .collect::<Result<HashMap<MemberIdentifier, Member>, ConversionError>>()?;
632
633        Ok(AssociationState {
634            inbox_id: proto.inbox_id,
635            members,
636            recovery_identifier,
637            seen_signatures: HashSet::from_iter(proto.seen_signatures),
638        })
639    }
640}
641
642impl From<AssociationStateDiff> for AssociationStateDiffProto {
643    fn from(diff: AssociationStateDiff) -> AssociationStateDiffProto {
644        AssociationStateDiffProto {
645            new_members: diff.new_members.into_iter().map(Into::into).collect(),
646            removed_members: diff.removed_members.into_iter().map(Into::into).collect(),
647        }
648    }
649}
650
651/// Convert a vector of `A` into a vector of `B` using [`From`]
652pub fn map_vec<A, B: From<A>>(other: Vec<A>) -> Vec<B> {
653    other.into_iter().map(B::from).collect()
654}
655
656/// Convert a vector of `A` into a vector of `B` using [`TryFrom`]
657/// Useful to convert vectors of structs into protos, like `Vec<IdentityUpdate>` to `Vec<IdentityUpdateProto>` or vice-versa.
658pub fn try_map_vec<A, B: TryFrom<A>>(other: Vec<A>) -> Result<Vec<B>, <B as TryFrom<A>>::Error> {
659    other.into_iter().map(B::try_from).collect()
660}
661
662// TODO:nm This doesn't really feel like serialization, maybe should move
663impl TryFrom<LegacySignedPublicKeyProto> for ValidatedLegacySignedPublicKey {
664    type Error = SignatureError;
665
666    fn try_from(proto: LegacySignedPublicKeyProto) -> Result<Self, Self::Error> {
667        let serialized_key_data = proto.key_bytes;
668        let union = proto
669            .signature
670            .ok_or(SignatureError::Invalid)?
671            .union
672            .ok_or(SignatureError::Invalid)?;
673        let wallet_signature = match union {
674            Union::WalletEcdsaCompact(wallet_ecdsa_compact) => {
675                let mut wallet_signature = wallet_ecdsa_compact.bytes.clone();
676                wallet_signature.push(wallet_ecdsa_compact.recovery as u8); // TODO: normalize recovery ID if necessary
677                if wallet_signature.len() != 65 {
678                    return Err(SignatureError::Invalid);
679                }
680                wallet_signature
681            }
682            Union::EcdsaCompact(ecdsa_compact) => {
683                let mut signature = ecdsa_compact.bytes.clone();
684                signature.push(ecdsa_compact.recovery as u8); // TODO: normalize recovery ID if necessary
685                if signature.len() != 65 {
686                    return Err(SignatureError::Invalid);
687                }
688                signature
689            }
690        };
691        let verified_wallet_signature = VerifiedSignature::from_recoverable_ecdsa(
692            Self::text(&serialized_key_data),
693            &wallet_signature,
694        )?;
695
696        let account_address = verified_wallet_signature.signer.to_string();
697        let account_address = sanitize_evm_addresses(&[account_address])?[0].clone();
698
699        let legacy_unsigned_public_key_proto =
700            LegacyUnsignedPublicKeyProto::decode(serialized_key_data.as_slice())
701                .or(Err(SignatureError::Invalid))?;
702        let public_key_bytes = match legacy_unsigned_public_key_proto
703            .union
704            .ok_or(SignatureError::Invalid)?
705        {
706            unsigned_public_key::Union::Secp256k1Uncompressed(secp256k1_uncompressed) => {
707                secp256k1_uncompressed.bytes
708            }
709        };
710        let created_ns = legacy_unsigned_public_key_proto.created_ns;
711
712        Ok(Self {
713            account_address,
714            wallet_signature: verified_wallet_signature,
715            serialized_key_data,
716            public_key_bytes,
717            created_ns,
718        })
719    }
720}
721
722impl From<ValidatedLegacySignedPublicKey> for LegacySignedPublicKeyProto {
723    fn from(validated: ValidatedLegacySignedPublicKey) -> Self {
724        let signature = validated.wallet_signature.raw_bytes;
725        Self {
726            key_bytes: validated.serialized_key_data,
727            signature: Some(SignedPublicKeySignatureProto {
728                union: Some(Union::WalletEcdsaCompact(WalletEcdsaCompact {
729                    bytes: signature[0..64].to_vec(),
730                    recovery: signature[64] as u32,
731                })),
732            }),
733        }
734    }
735}
736
737impl TryFrom<String> for AccountId {
738    type Error = ConversionError;
739
740    fn try_from(s: String) -> Result<Self, Self::Error> {
741        let parts: Vec<&str> = s.split(':').collect();
742        if parts.len() != 3 {
743            return Err(ConversionError::InvalidLength {
744                item: "account_id",
745                expected: 3,
746                got: parts.len(),
747            });
748        }
749        let chain_id = format!("{}:{}", parts[0], parts[1]);
750        let chain_id_regex = Regex::new(r"^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$")
751            .expect("Static regex should always compile");
752        let account_address = parts[2];
753        let account_address_regex =
754            Regex::new(r"^[-.%a-zA-Z0-9]{1,128}$").expect("static regex should always compile");
755        if !chain_id_regex.is_match(&chain_id) || !account_address_regex.is_match(account_address) {
756            return Err(ConversionError::InvalidValue {
757                item: "eth account_id",
758                expected: "well-formed chain_id & address",
759                got: s.to_string(),
760            });
761        }
762
763        Ok(AccountId {
764            chain_id: chain_id.to_string(),
765            account_address: account_address.to_string(),
766        })
767    }
768}
769
770impl TryFrom<&str> for AccountId {
771    type Error = ConversionError;
772
773    fn try_from(s: &str) -> Result<Self, Self::Error> {
774        s.to_string().try_into()
775    }
776}
777
778impl From<AccountId> for String {
779    fn from(account_id: AccountId) -> Self {
780        format!("{}:{}", account_id.chain_id, account_id.account_address)
781    }
782}
783
784#[cfg(test)]
785pub(crate) mod tests {
786    use xmtp_common::{rand_u64, rand_vec};
787
788    use super::*;
789
790    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
791    #[cfg_attr(not(target_arch = "wasm32"), test)]
792    fn test_round_trip_unverified() {
793        let account_identifier = Identifier::rand_ethereum();
794        let nonce = rand_u64();
795        let inbox_id = account_identifier.inbox_id(nonce).unwrap();
796        let client_timestamp_ns = rand_u64();
797        let signature_bytes = rand_vec::<32>();
798
799        let identity_update = UnverifiedIdentityUpdate::new(
800            inbox_id,
801            client_timestamp_ns,
802            vec![
803                UnverifiedAction::CreateInbox(UnverifiedCreateInbox {
804                    initial_identifier_signature: UnverifiedSignature::RecoverableEcdsa(
805                        UnverifiedRecoverableEcdsaSignature::new(signature_bytes),
806                    ),
807                    unsigned_action: UnsignedCreateInbox {
808                        nonce,
809                        account_identifier,
810                    },
811                }),
812                UnverifiedAction::AddAssociation(UnverifiedAddAssociation {
813                    new_member_signature: UnverifiedSignature::new_recoverable_ecdsa(vec![1, 2, 3]),
814                    existing_member_signature: UnverifiedSignature::new_recoverable_ecdsa(vec![
815                        4, 5, 6,
816                    ]),
817                    unsigned_action: UnsignedAddAssociation {
818                        new_member_identifier: MemberIdentifier::rand_ethereum(),
819                    },
820                }),
821                UnverifiedAction::ChangeRecoveryAddress(UnverifiedChangeRecoveryAddress {
822                    recovery_identifier_signature: UnverifiedSignature::new_recoverable_ecdsa(
823                        vec![7, 8, 9],
824                    ),
825                    unsigned_action: UnsignedChangeRecoveryAddress {
826                        new_recovery_identifier: Identifier::rand_ethereum(),
827                    },
828                }),
829                UnverifiedAction::RevokeAssociation(UnverifiedRevokeAssociation {
830                    recovery_identifier_signature: UnverifiedSignature::new_recoverable_ecdsa(
831                        vec![10, 11, 12],
832                    ),
833                    unsigned_action: UnsignedRevokeAssociation {
834                        revoked_member: MemberIdentifier::rand_ethereum(),
835                    },
836                }),
837            ],
838        );
839
840        let serialized_update = IdentityUpdateProto::from(identity_update.clone());
841
842        assert_eq!(
843            serialized_update.client_timestamp_ns,
844            identity_update.client_timestamp_ns
845        );
846        assert_eq!(serialized_update.actions.len(), 4);
847
848        let deserialized_update: UnverifiedIdentityUpdate = serialized_update
849            .clone()
850            .try_into()
851            .expect("deserialization error");
852
853        assert_eq!(deserialized_update, identity_update);
854
855        let reserialized = IdentityUpdateProto::from(deserialized_update);
856
857        assert_eq!(serialized_update, reserialized);
858    }
859
860    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
861    #[cfg_attr(not(target_arch = "wasm32"), test)]
862    fn test_account_id() {
863        // valid evm chain
864        let text = "eip155:1:0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb".to_string();
865        let account_id: AccountId = text.clone().try_into().unwrap();
866        assert_eq!(account_id.chain_id, "eip155:1");
867        assert_eq!(
868            account_id.account_address,
869            "0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb"
870        );
871        assert!(account_id.is_evm_chain());
872        let proto: String = account_id.into();
873        assert_eq!(text, proto);
874
875        // valid Bitcoin mainnet
876        let text = "bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6";
877        let account_id: AccountId = text.try_into().unwrap();
878        assert_eq!(
879            account_id.chain_id,
880            "bip122:000000000019d6689c085ae165831e93"
881        );
882        assert_eq!(
883            account_id.account_address,
884            "128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6"
885        );
886        assert!(!account_id.is_evm_chain());
887        let proto: String = account_id.into();
888        assert_eq!(text, proto);
889
890        // valid Cosmos Hub
891        let text = "cosmos:cosmoshub-3:cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0";
892        let account_id: AccountId = text.try_into().unwrap();
893        assert_eq!(account_id.chain_id, "cosmos:cosmoshub-3");
894        assert_eq!(
895            account_id.account_address,
896            "cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0"
897        );
898        assert!(!account_id.is_evm_chain());
899        let proto: String = account_id.into();
900        assert_eq!(text, proto);
901
902        // valid Kusama network
903        let text = "polkadot:b0a8d493285c2df73290dfb7e61f870f:5hmuyxw9xdgbpptgypokw4thfyoe3ryenebr381z9iaegmfy";
904        let account_id: AccountId = text.try_into().unwrap();
905        assert_eq!(
906            account_id.chain_id,
907            "polkadot:b0a8d493285c2df73290dfb7e61f870f"
908        );
909        assert_eq!(
910            account_id.account_address,
911            "5hmuyxw9xdgbpptgypokw4thfyoe3ryenebr381z9iaegmfy"
912        );
913        assert!(!account_id.is_evm_chain());
914        let proto: String = account_id.into();
915        assert_eq!(text, proto);
916
917        // valid StarkNet Testnet
918        let text =
919            "starknet:SN_GOERLI:0x02dd1b492765c064eac4039e3841aa5f382773b598097a40073bd8b48170ab57";
920        let account_id: AccountId = text.try_into().unwrap();
921        assert_eq!(account_id.chain_id, "starknet:SN_GOERLI");
922        assert_eq!(
923            account_id.account_address,
924            "0x02dd1b492765c064eac4039e3841aa5f382773b598097a40073bd8b48170ab57"
925        );
926        assert!(!account_id.is_evm_chain());
927        let proto: String = account_id.into();
928        assert_eq!(text, proto);
929
930        // dummy max length (64+1+8+1+32 = 106 chars/bytes)
931        let text = "chainstd:8c3444cf8970a9e41a706fab93e7a6c4:6d9b0b4b9994e8a6afbd3dc3ed983cd51c755afb27cd1dc7825ef59c134a39f7";
932        let account_id: AccountId = text.try_into().unwrap();
933        assert_eq!(
934            account_id.chain_id,
935            "chainstd:8c3444cf8970a9e41a706fab93e7a6c4"
936        );
937        assert_eq!(
938            account_id.account_address,
939            "6d9b0b4b9994e8a6afbd3dc3ed983cd51c755afb27cd1dc7825ef59c134a39f7"
940        );
941        assert!(!account_id.is_evm_chain());
942        let proto: String = account_id.into();
943        assert_eq!(text, proto);
944
945        // Hedera address (with optional checksum suffix per [HIP-15][])
946        let text = "hedera:mainnet:0.0.1234567890-zbhlt";
947        let account_id: AccountId = text.try_into().unwrap();
948        assert_eq!(account_id.chain_id, "hedera:mainnet");
949        assert_eq!(account_id.account_address, "0.0.1234567890-zbhlt");
950        assert!(!account_id.is_evm_chain());
951        let proto: String = account_id.into();
952        assert_eq!(text, proto);
953
954        // invalid
955        let text = "eip/155:1:0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcd";
956        let result: Result<AccountId, ConversionError> = text.try_into();
957        tracing::info!("{:?}", result);
958        assert!(matches!(
959            result,
960            Err(ConversionError::InvalidValue {
961                item: "eth account_id",
962                expected: "well-formed chain_id & address",
963                ..
964            })
965        ));
966    }
967
968    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
969    #[cfg_attr(not(target_arch = "wasm32"), test)]
970    fn test_account_id_create() {
971        let address = "0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb".to_string();
972        let chain_id = 12;
973        let account_id = AccountId::new_evm(chain_id, address.clone());
974        assert_eq!(account_id.account_address, address);
975        assert_eq!(account_id.chain_id, "eip155:12");
976    }
977}