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 #[error("Missing action")]
64 MissingAction,
65 #[error("Missing update")]
69 MissingUpdate,
70 #[error("Missing member identifier")]
74 MissingMemberIdentifier,
75 #[error("Missing signature")]
79 Signature,
80 #[error("Missing Member")]
84 MissingMember,
85 #[error("Decode error {0}")]
89 Decode(#[from] DecodeError),
90 #[error("Invalid account id")]
94 InvalidAccountId,
95 #[error("Invalid passkey")]
99 InvalidPasskey,
100 #[error("Invalid hash (needs to be 32 bytes)")]
104 InvalidHash,
105 #[error("A required field is unspecified: {0}")]
109 Unspecified(&'static str),
110 #[error("Field is deprecated: {0}")]
114 Deprecated(&'static str),
115 #[error("Error creating public key from proto bytes")]
119 Ed25519(#[from] ed25519_dalek::ed25519::Error),
120 #[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
651pub fn map_vec<A, B: From<A>>(other: Vec<A>) -> Vec<B> {
653 other.into_iter().map(B::from).collect()
654}
655
656pub 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
662impl 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); 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); 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 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 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 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 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 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 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 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 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}