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