1mod association_log;
2pub mod builder;
3pub mod ident;
4pub(super) mod member;
5pub(super) mod serialization;
6pub mod signature;
7pub(super) mod state;
8#[cfg(any(test, feature = "test-utils"))]
9pub mod test_utils;
10pub mod unsigned_actions;
11pub mod unverified;
12pub mod verified_signature;
13
14pub use self::association_log::*;
15pub use self::member::{HasMemberKind, Identifier, Member, MemberIdentifier, MemberKind};
16pub use self::serialization::{DeserializationError, map_vec, try_map_vec};
17pub use self::signature::*;
18pub use self::state::{AssociationState, AssociationStateDiff};
19
20pub fn apply_update(
22 initial_state: AssociationState,
23 update: IdentityUpdate,
24) -> Result<AssociationState, AssociationError> {
25 update.update_state(Some(initial_state), update.client_timestamp_ns)
26}
27
28pub fn get_state<Updates: AsRef<[IdentityUpdate]>>(
30 updates: Updates,
31) -> Result<AssociationState, AssociationError> {
32 let mut state = None;
33 for update in updates.as_ref().iter() {
34 let res = update.update_state(state, update.client_timestamp_ns);
35 state = Some(res?);
36 }
37
38 state.ok_or(AssociationError::NotCreated)
39}
40
41#[cfg(any(test, feature = "test-utils"))]
42pub mod test_defaults {
43 use self::{
44 unverified::{UnverifiedAction, UnverifiedIdentityUpdate},
45 verified_signature::VerifiedSignature,
46 };
47 use super::{member::Identifier, *};
48 use xmtp_common::{rand_u64, rand_vec};
49
50 impl IdentityUpdate {
51 pub fn new_test(actions: Vec<Action>, inbox_id: String) -> Self {
52 Self::new(actions, inbox_id, rand_u64())
53 }
54 }
55
56 impl UnverifiedIdentityUpdate {
57 pub fn new_test(actions: Vec<UnverifiedAction>, inbox_id: String) -> Self {
58 Self::new(inbox_id, rand_u64(), actions)
59 }
60 }
61
62 impl Default for AddAssociation {
63 fn default() -> Self {
64 let existing_member = Identifier::rand_ethereum();
65 let new_member = MemberIdentifier::rand_installation();
66 Self {
67 existing_member_signature: VerifiedSignature::new(
68 existing_member.into(),
69 SignatureKind::Erc191,
70 rand_vec::<32>(),
71 None,
72 ),
73 new_member_signature: VerifiedSignature::new(
74 new_member.clone(),
75 SignatureKind::InstallationKey,
76 rand_vec::<32>(),
77 None,
78 ),
79 new_member_identifier: new_member,
80 }
81 }
82 }
83
84 impl Default for CreateInbox {
86 fn default() -> Self {
87 let signer = Identifier::rand_ethereum();
88 Self {
89 nonce: rand_u64(),
90 account_identifier: signer.clone(),
91 initial_identifier_signature: VerifiedSignature::new(
92 signer.into(),
93 SignatureKind::Erc191,
94 rand_vec::<32>(),
95 None,
96 ),
97 }
98 }
99 }
100
101 impl Default for RevokeAssociation {
102 fn default() -> Self {
103 let signer = MemberIdentifier::rand_ethereum();
104 Self {
105 recovery_identifier_signature: VerifiedSignature::new(
106 signer,
107 SignatureKind::Erc191,
108 rand_vec::<32>(),
109 None,
110 ),
111 revoked_member: MemberIdentifier::rand_ethereum(),
112 }
113 }
114 }
115}
116
117#[cfg(test)]
118pub(crate) mod tests {
119 #[cfg(target_arch = "wasm32")]
120 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);
121 use wasm_bindgen_test::wasm_bindgen_test;
122
123 use super::*;
124 use crate::associations::{member::Identifier, verified_signature::VerifiedSignature};
125 use xmtp_common::{rand_hexstring, rand_vec};
126
127 pub fn new_test_inbox() -> AssociationState {
128 let create_request = CreateInbox::default();
129 let inbox_id = create_request
130 .account_identifier
131 .inbox_id(create_request.nonce)
132 .unwrap();
133
134 let identity_update =
135 IdentityUpdate::new_test(vec![Action::CreateInbox(create_request)], inbox_id);
136
137 get_state(vec![identity_update]).unwrap()
138 }
139
140 pub fn new_test_inbox_with_installation() -> AssociationState {
141 let initial_state = new_test_inbox();
142 let inbox_id = initial_state.inbox_id().to_string();
143 let initial_wallet_address = initial_state.recovery_identifier.clone();
144
145 let update = Action::AddAssociation(AddAssociation {
146 existing_member_signature: VerifiedSignature::new(
147 initial_wallet_address.clone().into(),
148 SignatureKind::Erc191,
149 rand_vec::<32>(),
150 None,
151 ),
152 ..Default::default()
153 });
154
155 apply_update(
156 initial_state,
157 IdentityUpdate::new_test(vec![update], inbox_id.to_string()),
158 )
159 .unwrap()
160 }
161
162 #[wasm_bindgen_test(unsupported = test)]
163 fn test_create_inbox() {
164 let create_request = CreateInbox::default();
165 let inbox_id = create_request
166 .account_identifier
167 .inbox_id(create_request.nonce)
168 .unwrap();
169
170 let account_address = create_request.account_identifier.clone();
171 let identity_update =
172 IdentityUpdate::new_test(vec![Action::CreateInbox(create_request)], inbox_id.clone());
173 let state = get_state(vec![identity_update]).unwrap();
174 assert_eq!(state.members().len(), 1);
175
176 let existing_entity = state.get(&account_address.clone().into()).unwrap();
177 assert_eq!(existing_entity.identifier, account_address);
178 }
179
180 #[wasm_bindgen_test(unsupported = test)]
181 fn create_and_add_separately() {
182 let initial_state = new_test_inbox();
183 let inbox_id = initial_state.inbox_id().to_string();
184 let new_installation_identifier = MemberIdentifier::rand_installation();
185 let first_member = initial_state.recovery_identifier.clone();
186
187 let update = Action::AddAssociation(AddAssociation {
188 new_member_identifier: new_installation_identifier.clone(),
189 new_member_signature: VerifiedSignature::new(
190 new_installation_identifier.clone(),
191 SignatureKind::InstallationKey,
192 rand_vec::<32>(),
193 None,
194 ),
195 existing_member_signature: VerifiedSignature::new(
196 first_member.clone().into(),
197 SignatureKind::Erc191,
198 rand_vec::<32>(),
199 None,
200 ),
201 });
202
203 let new_state = apply_update(
204 initial_state,
205 IdentityUpdate::new_test(vec![update], inbox_id.to_string()),
206 )
207 .unwrap();
208 assert_eq!(new_state.members().len(), 2);
209
210 let new_member = new_state.get(&new_installation_identifier).unwrap();
211 assert_eq!(new_member.added_by_entity, Some(first_member.into()));
212 }
213
214 #[wasm_bindgen_test(unsupported = test)]
215 fn create_and_add_together() {
216 let create_action = CreateInbox::default();
217 let account_address = create_action.account_identifier.clone();
218 let inbox_id = account_address.inbox_id(create_action.nonce).unwrap();
219 let new_member_identifier = MemberIdentifier::rand_installation();
220 let add_action = AddAssociation {
221 existing_member_signature: VerifiedSignature::new(
222 account_address.clone().into(),
223 SignatureKind::Erc191,
224 rand_vec::<32>(),
225 None,
226 ),
227 new_member_signature: VerifiedSignature::new(
229 new_member_identifier.clone(),
230 SignatureKind::InstallationKey,
231 rand_vec::<32>(),
232 None,
233 ),
234 new_member_identifier: new_member_identifier.clone(),
235 };
236 let identity_update = IdentityUpdate::new_test(
237 vec![
238 Action::CreateInbox(create_action),
239 Action::AddAssociation(add_action),
240 ],
241 inbox_id.clone(),
242 );
243 let state = get_state(vec![identity_update]).unwrap();
244 assert_eq!(state.members().len(), 2);
245 assert_eq!(
246 state.get(&new_member_identifier).unwrap().added_by_entity,
247 Some(account_address.into())
248 );
249 }
250
251 #[wasm_bindgen_test(unsupported = test)]
252 fn create_from_legacy_key() {
253 let member_identifier = Identifier::rand_ethereum();
254 let create_action = CreateInbox {
255 nonce: 0,
256 account_identifier: member_identifier.clone(),
257 initial_identifier_signature: VerifiedSignature::new(
258 member_identifier.clone().into(),
259 SignatureKind::LegacyDelegated,
260 "0".as_bytes().to_vec(),
261 None,
262 ),
263 };
264 let inbox_id = member_identifier.inbox_id(0).unwrap();
265 let state = get_state(vec![IdentityUpdate::new_test(
266 vec![Action::CreateInbox(create_action)],
267 inbox_id.clone(),
268 )])
269 .unwrap();
270 assert_eq!(state.members().len(), 1);
271
272 let update = Action::AddAssociation(AddAssociation {
274 existing_member_signature: VerifiedSignature::new(
275 member_identifier.into(),
276 SignatureKind::LegacyDelegated,
277 "0".as_bytes().to_vec(),
279 None,
280 ),
281 ..Default::default()
282 });
283 let update_result = apply_update(
284 state,
285 IdentityUpdate::new_test(vec![update], inbox_id.clone()),
286 );
287 assert!(matches!(update_result, Err(AssociationError::Replay)));
288 }
289
290 #[wasm_bindgen_test(unsupported = test)]
291 fn add_wallet_from_installation_key() {
292 let initial_state = new_test_inbox_with_installation();
293 let inbox_id = initial_state.inbox_id().to_string();
294 let installation_id = initial_state
295 .members_by_kind(MemberKind::Installation)
296 .first()
297 .cloned()
298 .unwrap()
299 .identifier;
300
301 let new_wallet_address = MemberIdentifier::rand_ethereum();
302 let add_association = Action::AddAssociation(AddAssociation {
303 new_member_identifier: new_wallet_address.clone(),
304 new_member_signature: VerifiedSignature::new(
305 new_wallet_address.clone(),
306 SignatureKind::Erc191,
307 rand_vec::<32>(),
308 None,
309 ),
310 existing_member_signature: VerifiedSignature::new(
311 installation_id.clone(),
312 SignatureKind::InstallationKey,
313 rand_vec::<32>(),
314 None,
315 ),
316 });
317
318 let new_state = apply_update(
319 initial_state,
320 IdentityUpdate::new_test(vec![add_association], inbox_id.to_string()),
321 )
322 .expect("expected update to succeed");
323 assert_eq!(new_state.members().len(), 3);
324 }
325
326 #[wasm_bindgen_test(unsupported = test)]
327 fn reject_invalid_signature_on_create() {
328 let bad_signature = VerifiedSignature::new(
330 MemberIdentifier::rand_ethereum(),
331 SignatureKind::Erc191,
332 rand_vec::<32>(),
333 None,
334 );
335 let action = CreateInbox {
336 initial_identifier_signature: bad_signature,
337 ..Default::default()
338 };
339
340 let state_result = get_state(vec![IdentityUpdate::new_test(
341 vec![Action::CreateInbox(action)],
342 rand_hexstring(),
343 )]);
344
345 assert!(state_result.is_err());
346 assert!(matches!(
347 state_result,
348 Err(AssociationError::MissingExistingMember)
349 ));
350 }
351
352 #[wasm_bindgen_test(unsupported = test)]
353 fn reject_invalid_signature_on_update() {
354 let initial_state = new_test_inbox();
355 let inbox_id = initial_state.inbox_id().to_string();
356 let bad_signature = VerifiedSignature::new(
358 MemberIdentifier::rand_ethereum(),
359 SignatureKind::Erc191,
360 rand_vec::<32>(),
361 None,
362 );
363
364 let update_with_bad_existing_member = Action::AddAssociation(AddAssociation {
365 existing_member_signature: bad_signature.clone(),
366 ..Default::default()
367 });
368
369 let update_result = apply_update(
370 initial_state.clone(),
371 IdentityUpdate::new_test(vec![update_with_bad_existing_member], inbox_id.to_string()),
372 );
373
374 assert!(matches!(
375 update_result,
376 Err(AssociationError::MissingExistingMember)
377 ));
378
379 let update_with_bad_new_member = Action::AddAssociation(AddAssociation {
380 new_member_signature: bad_signature.clone(),
381 existing_member_signature: VerifiedSignature::new(
382 initial_state.recovery_identifier().clone().into(),
383 SignatureKind::Erc191,
384 rand_vec::<32>(),
385 None,
386 ),
387 ..Default::default()
388 });
389
390 let update_result_2 = apply_update(
391 initial_state,
392 IdentityUpdate::new_test(vec![update_with_bad_new_member], inbox_id.to_string()),
393 );
394 assert!(matches!(
395 update_result_2,
396 Err(AssociationError::NewMemberIdSignatureMismatch)
397 ));
398 }
399
400 #[wasm_bindgen_test(unsupported = test)]
401 fn reject_if_signer_not_existing_member() {
402 let create_inbox = CreateInbox::default();
403 let inbox_id = create_inbox
404 .account_identifier
405 .inbox_id(create_inbox.nonce)
406 .unwrap();
407
408 let create_request = Action::CreateInbox(create_inbox);
409 let update = Action::AddAssociation(AddAssociation {
411 existing_member_signature: VerifiedSignature::new(
413 MemberIdentifier::rand_ethereum(),
414 SignatureKind::Erc191,
415 rand_vec::<32>(),
416 None,
417 ),
418 ..Default::default()
419 });
420
421 let state_result = get_state(vec![IdentityUpdate::new_test(
422 vec![create_request, update],
423 inbox_id.clone(),
424 )]);
425 assert!(matches!(
426 state_result,
427 Err(AssociationError::MissingExistingMember)
428 ));
429 }
430
431 #[wasm_bindgen_test(unsupported = test)]
432 fn reject_if_installation_adding_installation() {
433 let existing_state = new_test_inbox_with_installation();
434 let inbox_id = existing_state.inbox_id().to_string();
435 let existing_installations = existing_state.members_by_kind(MemberKind::Installation);
436 let existing_installation = existing_installations.first().unwrap();
437 let new_installation_id = MemberIdentifier::rand_installation();
438
439 let update = Action::AddAssociation(AddAssociation {
440 existing_member_signature: VerifiedSignature::new(
441 existing_installation.identifier.clone(),
442 SignatureKind::InstallationKey,
443 rand_vec::<32>(),
444 None,
445 ),
446 new_member_identifier: new_installation_id.clone(),
447 new_member_signature: VerifiedSignature::new(
448 new_installation_id.clone(),
449 SignatureKind::InstallationKey,
450 rand_vec::<32>(),
451 None,
452 ),
453 });
454
455 let update_result = apply_update(
456 existing_state,
457 IdentityUpdate::new_test(vec![update], inbox_id.to_string()),
458 );
459 assert!(matches!(
460 update_result,
461 Err(AssociationError::MemberNotAllowed(
462 MemberKind::Installation,
463 MemberKind::Installation
464 ))
465 ));
466 }
467
468 #[wasm_bindgen_test(unsupported = test)]
469 fn revoke() {
470 let initial_state = new_test_inbox_with_installation();
471 let inbox_id = initial_state.inbox_id().to_string();
472 let installation_id = initial_state
473 .members_by_kind(MemberKind::Installation)
474 .first()
475 .cloned()
476 .unwrap()
477 .identifier;
478 let update = Action::RevokeAssociation(RevokeAssociation {
479 recovery_identifier_signature: VerifiedSignature::new(
480 initial_state.recovery_identifier.clone().into(),
481 SignatureKind::Erc191,
482 rand_vec::<32>(),
483 None,
484 ),
485 revoked_member: installation_id.clone(),
486 });
487
488 let new_state = apply_update(
489 initial_state,
490 IdentityUpdate::new_test(vec![update], inbox_id.to_string()),
491 )
492 .expect("expected update to succeed");
493 assert!(new_state.get(&installation_id).is_none());
494 }
495
496 #[wasm_bindgen_test(unsupported = test)]
497 fn revoke_children() {
498 let initial_state = new_test_inbox_with_installation();
499 let inbox_id = initial_state.inbox_id().to_string();
500 let wallet_address = initial_state
501 .members_by_kind(MemberKind::Ethereum)
502 .first()
503 .cloned()
504 .unwrap()
505 .identifier;
506
507 let add_second_installation = Action::AddAssociation(AddAssociation {
508 existing_member_signature: VerifiedSignature::new(
509 wallet_address.clone(),
510 SignatureKind::Erc191,
511 rand_vec::<32>(),
512 None,
513 ),
514 ..Default::default()
515 });
516
517 let new_state = apply_update(
518 initial_state,
519 IdentityUpdate::new_test(vec![add_second_installation], inbox_id.to_string()),
520 )
521 .expect("expected update to succeed");
522 assert_eq!(new_state.members().len(), 3);
523
524 let revocation = Action::RevokeAssociation(RevokeAssociation {
525 recovery_identifier_signature: VerifiedSignature::new(
526 wallet_address.clone(),
527 SignatureKind::Erc191,
528 rand_vec::<32>(),
529 None,
530 ),
531 revoked_member: wallet_address.clone(),
532 });
533
534 let new_state = apply_update(
536 new_state,
537 IdentityUpdate::new_test(vec![revocation], inbox_id.to_string()),
538 )
539 .expect("expected update to succeed");
540 assert_eq!(new_state.members().len(), 0);
541 }
542
543 #[wasm_bindgen_test(unsupported = test)]
544 fn revoke_and_re_add() {
545 let initial_state = new_test_inbox();
546 let wallet_address = initial_state
547 .members_by_kind(MemberKind::Ethereum)
548 .first()
549 .cloned()
550 .unwrap()
551 .identifier;
552
553 let inbox_id = initial_state.inbox_id().to_string();
554
555 let second_wallet_address = MemberIdentifier::rand_ethereum();
556 let add_second_wallet = Action::AddAssociation(AddAssociation {
557 new_member_identifier: second_wallet_address.clone(),
558 new_member_signature: VerifiedSignature::new(
559 second_wallet_address.clone(),
560 SignatureKind::Erc191,
561 rand_vec::<32>(),
562 None,
563 ),
564 existing_member_signature: VerifiedSignature::new(
565 wallet_address.clone(),
566 SignatureKind::Erc191,
567 rand_vec::<32>(),
568 None,
569 ),
570 });
571
572 let revoke_second_wallet = Action::RevokeAssociation(RevokeAssociation {
573 recovery_identifier_signature: VerifiedSignature::new(
574 wallet_address.clone(),
575 SignatureKind::Erc191,
576 rand_vec::<32>(),
577 None,
578 ),
579 revoked_member: second_wallet_address.clone(),
580 });
581
582 let state_after_remove = apply_update(
583 initial_state,
584 IdentityUpdate::new_test(
585 vec![add_second_wallet, revoke_second_wallet],
586 inbox_id.to_string(),
587 ),
588 )
589 .expect("expected update to succeed");
590 assert_eq!(state_after_remove.members().len(), 1);
591
592 let add_second_wallet_again = Action::AddAssociation(AddAssociation {
593 new_member_identifier: second_wallet_address.clone(),
594 new_member_signature: VerifiedSignature::new(
595 second_wallet_address.clone(),
596 SignatureKind::Erc191,
597 rand_vec::<32>(),
598 None,
599 ),
600 existing_member_signature: VerifiedSignature::new(
601 wallet_address,
602 SignatureKind::Erc191,
603 rand_vec::<32>(),
604 None,
605 ),
606 });
607
608 let state_after_re_add = apply_update(
609 state_after_remove,
610 IdentityUpdate::new_test(vec![add_second_wallet_again], inbox_id.to_string()),
611 )
612 .expect("expected update to succeed");
613 assert_eq!(state_after_re_add.members().len(), 2);
614 }
615
616 #[wasm_bindgen_test(unsupported = test)]
617 fn change_recovery_address() {
618 let initial_state = new_test_inbox_with_installation();
619 let inbox_id = initial_state.inbox_id().to_string();
620 let initial_recovery_address = initial_state.recovery_identifier().clone();
621 let new_recovery_identifier = Identifier::rand_ethereum();
622 let update_recovery = Action::ChangeRecoveryIdentity(ChangeRecoveryIdentity {
623 new_recovery_identifier: new_recovery_identifier.clone(),
624 recovery_identifier_signature: VerifiedSignature::new(
625 initial_state.recovery_identifier().clone().into(),
626 SignatureKind::Erc191,
627 rand_vec::<32>(),
628 None,
629 ),
630 });
631
632 let new_state = apply_update(
633 initial_state,
634 IdentityUpdate::new_test(vec![update_recovery], inbox_id.to_string()),
635 )
636 .expect("expected update to succeed");
637 assert_eq!(new_state.recovery_identifier(), &new_recovery_identifier);
638
639 let attempted_revoke = Action::RevokeAssociation(RevokeAssociation {
640 recovery_identifier_signature: VerifiedSignature::new(
641 initial_recovery_address.clone().into(),
642 SignatureKind::Erc191,
643 rand_vec::<32>(),
644 None,
645 ),
646 revoked_member: initial_recovery_address.clone().into(),
647 });
648
649 let revoke_result = apply_update(
650 new_state,
651 IdentityUpdate::new_test(vec![attempted_revoke], inbox_id.to_string()),
652 );
653 assert!(revoke_result.is_err());
654 assert!(matches!(
655 revoke_result,
656 Err(AssociationError::MissingExistingMember)
657 ));
658 }
659
660 #[wasm_bindgen_test(unsupported = test)]
661 fn scw_signature_binding() {
662 let initial_chain_id: u64 = 1;
663 let signer = Identifier::rand_ethereum();
664 let initial_identifier_signature = VerifiedSignature::new(
665 signer.clone().into(),
666 SignatureKind::Erc1271,
667 rand_vec::<32>(),
668 Some(initial_chain_id),
669 );
670 let action = CreateInbox {
671 initial_identifier_signature,
672 nonce: 0,
673 account_identifier: signer.clone(),
674 };
675
676 let inbox_id = signer.inbox_id(0).unwrap();
677 let initial_state = get_state(vec![IdentityUpdate::new_test(
678 vec![Action::CreateInbox(action)],
679 inbox_id,
680 )])
681 .expect("initial state should be OK");
682
683 let inbox_id = initial_state.inbox_id();
684
685 let new_chain_id: u64 = 2;
686 let new_member = MemberIdentifier::rand_installation();
687
688 let existing_member_sig = VerifiedSignature::new(
690 signer.clone().into(),
691 SignatureKind::Erc1271,
692 rand_vec::<32>(),
693 Some(new_chain_id),
694 );
695
696 let actions: Vec<Action> = vec![
697 Action::AddAssociation(AddAssociation {
698 existing_member_signature: existing_member_sig.clone(),
699 new_member_signature: VerifiedSignature::new(
700 new_member.clone(),
701 SignatureKind::InstallationKey,
702 rand_vec::<32>(),
703 None,
704 ),
705 new_member_identifier: new_member.clone(),
706 }),
707 Action::RevokeAssociation(RevokeAssociation {
708 recovery_identifier_signature: existing_member_sig.clone(),
709 revoked_member: signer.clone().into(),
710 }),
711 Action::ChangeRecoveryIdentity(ChangeRecoveryIdentity {
712 recovery_identifier_signature: existing_member_sig.clone(),
713 new_recovery_identifier: Identifier::rand_ethereum(),
714 }),
715 ];
716
717 for action in actions {
719 let apply_result = apply_update(
720 initial_state.clone(),
721 IdentityUpdate::new_test(vec![action], inbox_id.to_string()),
722 );
723
724 assert!(matches!(
725 apply_result,
726 Err(AssociationError::ChainIdMismatch(_, _))
727 ));
728 }
729 }
730}