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