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