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
609pub fn map_vec<A, B: From<A>>(other: Vec<A>) -> Vec<B> {
611 other.into_iter().map(B::from).collect()
612}
613
614pub 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
620impl 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); 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); 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 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 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 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 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 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 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 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 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}