1use std::collections::HashMap;
6
7use super::member::{HasMemberKind, Identifier};
8use crate::scw_verifier::SmartContractSignatureVerifier;
9use thiserror::Error;
10use xmtp_common::ErrorCode;
11use xmtp_common::time::now_ns;
12
13use super::{
14 AccountId, Handle, MemberIdentifier, MemberKind, SignatureError,
15 unsigned_actions::{
16 SignatureTextCreator, UnsignedAction, UnsignedAddAssociation,
17 UnsignedChangeRecoveryAddress, UnsignedClaimHandle, UnsignedCreateInbox,
18 UnsignedIdentityUpdate, UnsignedRevokeAssociation,
19 },
20 unverified::{
21 NewUnverifiedSmartContractWalletSignature, UnverifiedAction, UnverifiedAddAssociation,
22 UnverifiedChangeRecoveryAddress, UnverifiedClaimHandle, UnverifiedCreateInbox,
23 UnverifiedIdentityUpdate, UnverifiedRevokeAssociation, UnverifiedSignature,
24 UnverifiedSmartContractWalletSignature,
25 },
26 verified_signature::VerifiedSignature,
27};
28
29#[derive(Clone, PartialEq, Hash, Eq, Debug)]
32enum SignatureField {
33 InitialAddress,
34 ExistingMember,
35 NewMember,
36 RecoveryAddress,
37}
38
39#[derive(Clone, Debug)]
40pub struct PendingIdentityAction {
41 unsigned_action: UnsignedAction,
42 pending_signatures: HashMap<SignatureField, MemberIdentifier>,
43}
44
45pub struct SignatureRequestBuilder {
50 inbox_id: String,
51 client_timestamp_ns: u64,
52 actions: Vec<PendingIdentityAction>,
53}
54
55impl SignatureRequestBuilder {
56 pub fn new<S: AsRef<str>>(inbox_id: S) -> Self {
58 Self {
59 inbox_id: inbox_id.as_ref().to_string(),
60 client_timestamp_ns: now_ns() as u64,
61 actions: vec![],
62 }
63 }
64
65 pub fn create_inbox(mut self, signer_identity: Identifier, nonce: u64) -> Self {
67 let pending_action = PendingIdentityAction {
68 unsigned_action: UnsignedAction::CreateInbox(UnsignedCreateInbox {
69 account_identifier: signer_identity.clone(),
70 nonce,
71 }),
72 pending_signatures: HashMap::from([(
73 SignatureField::InitialAddress,
74 signer_identity.into(),
75 )]),
76 };
77 self.actions.push(pending_action);
79
80 self
81 }
82
83 pub fn add_association(
85 mut self,
86 new_member_identifier: MemberIdentifier,
87 existing_member_identifier: MemberIdentifier,
88 ) -> Self {
89 self.actions.push(PendingIdentityAction {
90 unsigned_action: UnsignedAction::AddAssociation(UnsignedAddAssociation {
91 new_member_identifier: new_member_identifier.clone(),
92 }),
93 pending_signatures: HashMap::from([
94 (SignatureField::ExistingMember, existing_member_identifier),
95 (SignatureField::NewMember, new_member_identifier),
96 ]),
97 });
98
99 self
100 }
101
102 pub fn revoke_association(
103 mut self,
104 recovery_address_signer: MemberIdentifier,
105 revoked_member: MemberIdentifier,
106 ) -> Self {
107 self.actions.push(PendingIdentityAction {
108 pending_signatures: HashMap::from([(
109 SignatureField::RecoveryAddress,
110 recovery_address_signer,
111 )]),
112 unsigned_action: UnsignedAction::RevokeAssociation(UnsignedRevokeAssociation {
113 revoked_member,
114 }),
115 });
116
117 self
118 }
119
120 pub fn change_recovery_address(
121 mut self,
122 recovery_address_signer: MemberIdentifier,
123 new_recovery_identifier: Identifier,
124 ) -> Self {
125 self.actions.push(PendingIdentityAction {
126 pending_signatures: HashMap::from([(
127 SignatureField::RecoveryAddress,
128 recovery_address_signer,
129 )]),
130 unsigned_action: UnsignedAction::ChangeRecoveryAddress(UnsignedChangeRecoveryAddress {
131 new_recovery_identifier,
132 }),
133 });
134
135 self
136 }
137
138 pub fn claim_handle(mut self, handle: Handle, signer: MemberIdentifier) -> Self {
142 self.actions.push(PendingIdentityAction {
143 pending_signatures: HashMap::from([(SignatureField::ExistingMember, signer)]),
144 unsigned_action: UnsignedAction::ClaimHandle(UnsignedClaimHandle {
145 handle: handle.into(),
146 }),
147 });
148
149 self
150 }
151
152 pub fn build(self) -> SignatureRequest {
153 let unsigned_actions: Vec<UnsignedAction> = self
154 .actions
155 .iter()
156 .map(|pending_action| pending_action.unsigned_action.clone())
157 .collect();
158
159 let signature_text = get_signature_text(
160 unsigned_actions,
161 self.inbox_id.clone(),
162 self.client_timestamp_ns,
163 );
164
165 SignatureRequest::new(
166 self.actions,
167 signature_text,
168 self.inbox_id,
169 self.client_timestamp_ns,
170 )
171 }
172}
173
174#[derive(Debug, Error, ErrorCode)]
175pub enum SignatureRequestError {
176 #[error("Unknown signer")]
180 UnknownSigner,
181 #[error("Required signature was not provided")]
185 MissingSigner,
186 #[error("Signature error {0}")]
187 #[error_code(inherit)]
188 Signature(#[from] SignatureError),
189 #[error("Unable to get block number")]
193 BlockNumber,
194 #[error("the backend does not accept chain {chain}; it accepts {accepted:?}")]
199 ChainNotAccepted {
200 chain: String,
201 accepted: Vec<String>,
202 },
203}
204
205#[derive(Clone, Debug)]
210pub struct SignatureRequest {
211 pending_actions: Vec<PendingIdentityAction>,
212 signature_text: String,
213 signatures: HashMap<MemberIdentifier, UnverifiedSignature>,
214 accepted_chains: Option<std::sync::Arc<[String]>>,
219 client_timestamp_ns: u64,
220 inbox_id: String,
221}
222
223impl SignatureRequest {
224 pub fn new(
225 pending_actions: Vec<PendingIdentityAction>,
226 signature_text: String,
227 inbox_id: String,
228 client_timestamp_ns: u64,
229 ) -> Self {
230 Self {
231 inbox_id,
232 pending_actions,
233 signature_text,
234 signatures: HashMap::new(),
235 accepted_chains: None,
236 client_timestamp_ns,
237 }
238 }
239
240 pub fn missing_signatures(&self) -> Vec<&MemberIdentifier> {
241 self.pending_actions
242 .iter()
243 .flat_map(|pending_action| pending_action.pending_signatures.values())
244 .filter(|ident| !self.signatures.contains_key(ident))
245 .collect()
246 }
247
248 pub fn missing_address_signatures(&self) -> Vec<&MemberIdentifier> {
249 self.missing_signatures()
250 .into_iter()
251 .filter(|member| matches!(member.kind(), MemberKind::Ethereum | MemberKind::Passkey))
252 .collect()
253 }
254
255 pub fn restrict_chains(&mut self, chains: std::sync::Arc<[String]>) {
262 self.accepted_chains = Some(chains);
263 }
264
265 pub fn accepted_chains(&self) -> Option<&[String]> {
269 self.accepted_chains.as_deref()
270 }
271
272 fn check_chain(&self, account_id: &AccountId) -> Result<(), SignatureRequestError> {
277 let Some(accepted) = self.accepted_chains.as_ref() else {
278 return Ok(());
279 };
280 let chain = account_id.get_chain_id();
281 if accepted.iter().any(|accepted| accepted == chain) {
282 return Ok(());
283 }
284 Err(SignatureRequestError::ChainNotAccepted {
285 chain: chain.to_string(),
286 accepted: accepted.to_vec(),
287 })
288 }
289
290 pub async fn add_new_unverified_smart_contract_signature(
291 &mut self,
292 mut signature: NewUnverifiedSmartContractWalletSignature,
293 scw_verifier: impl SmartContractSignatureVerifier,
294 ) -> Result<(), SignatureRequestError> {
295 self.check_chain(&signature.account_id)?;
296 let verified_signature = VerifiedSignature::from_smart_contract_wallet(
297 &self.signature_text,
298 scw_verifier,
299 &signature.signature_bytes,
300 signature.account_id.clone(),
301 &mut signature.block_number,
302 )
303 .await?;
304
305 let Some(block_number) = signature.block_number else {
306 return Err(SignatureRequestError::BlockNumber);
307 };
308
309 self.add_verified_signature(
310 UnverifiedSignature::SmartContractWallet(UnverifiedSmartContractWalletSignature {
311 account_id: signature.account_id,
312 block_number,
313 signature_bytes: signature.signature_bytes,
314 }),
315 verified_signature,
316 )
317 }
318
319 pub async fn add_signature(
320 &mut self,
321 signature: UnverifiedSignature,
322 scw_verifier: impl SmartContractSignatureVerifier,
323 ) -> Result<(), SignatureRequestError> {
324 if let UnverifiedSignature::SmartContractWallet(scw) = &signature {
325 self.check_chain(&scw.account_id)?;
326 }
327 let verified_signature = signature
328 .to_verified(self.signature_text.clone(), scw_verifier)
329 .await?;
330
331 self.add_verified_signature(signature, verified_signature)
332 }
333
334 fn add_verified_signature(
335 &mut self,
336 signature: UnverifiedSignature,
337 verified_signature: VerifiedSignature,
338 ) -> Result<(), SignatureRequestError> {
339 let signer_identity = &verified_signature.signer;
340
341 let missing_signatures = self.missing_signatures();
342 tracing::info!(
343 signer = %signer_identity,
344 missing_signatures=?missing_signatures,
345 "adding verified signature");
346
347 if !missing_signatures.contains(&signer_identity) {
349 return Err(SignatureRequestError::UnknownSigner);
350 }
351
352 self.signatures.insert(verified_signature.signer, signature);
353
354 Ok(())
355 }
356
357 pub fn is_ready(&self) -> bool {
358 self.missing_signatures().is_empty()
359 }
360
361 pub fn signature_text(&self) -> String {
362 self.signature_text.clone()
363 }
364
365 pub fn build_identity_update(self) -> Result<UnverifiedIdentityUpdate, SignatureRequestError> {
366 if !self.is_ready() {
367 return Err(SignatureRequestError::MissingSigner);
368 }
369
370 let actions = self
371 .pending_actions
372 .clone()
373 .into_iter()
374 .map(|pending_action| build_action(pending_action, &self.signatures))
375 .collect::<Result<Vec<UnverifiedAction>, SignatureRequestError>>()?;
376
377 Ok(UnverifiedIdentityUpdate::new(
378 self.inbox_id,
379 self.client_timestamp_ns,
380 actions,
381 ))
382 }
383
384 pub fn inbox_id(&self) -> crate::InboxIdRef<'_> {
385 &self.inbox_id
386 }
387}
388
389fn build_action(
390 pending_action: PendingIdentityAction,
391 signatures: &HashMap<MemberIdentifier, UnverifiedSignature>,
392) -> Result<UnverifiedAction, SignatureRequestError> {
393 match pending_action.unsigned_action {
394 UnsignedAction::CreateInbox(unsigned_action) => {
395 let signer_identity = pending_action
396 .pending_signatures
397 .get(&SignatureField::InitialAddress)
398 .ok_or(SignatureRequestError::MissingSigner)?;
399 let initial_identifier_signature = signatures
400 .get(signer_identity)
401 .cloned()
402 .ok_or(SignatureRequestError::MissingSigner)?;
403
404 Ok(UnverifiedAction::CreateInbox(UnverifiedCreateInbox {
405 unsigned_action,
406 initial_identifier_signature,
407 }))
408 }
409 UnsignedAction::AddAssociation(unsigned_action) => {
410 let existing_member_signer_identity = pending_action
411 .pending_signatures
412 .get(&SignatureField::ExistingMember)
413 .ok_or(SignatureRequestError::MissingSigner)?;
414 let new_member_signer_identity = pending_action
415 .pending_signatures
416 .get(&SignatureField::NewMember)
417 .ok_or(SignatureRequestError::MissingSigner)?;
418
419 let existing_member_signature = signatures
420 .get(existing_member_signer_identity)
421 .cloned()
422 .ok_or(SignatureRequestError::MissingSigner)?;
423
424 let new_member_signature = signatures
425 .get(new_member_signer_identity)
426 .cloned()
427 .ok_or(SignatureRequestError::MissingSigner)?;
428
429 Ok(UnverifiedAction::AddAssociation(UnverifiedAddAssociation {
430 unsigned_action,
431 existing_member_signature,
432 new_member_signature,
433 }))
434 }
435 UnsignedAction::RevokeAssociation(unsigned_action) => {
436 let signer_identity = pending_action
437 .pending_signatures
438 .get(&SignatureField::RecoveryAddress)
439 .ok_or(SignatureRequestError::MissingSigner)?;
440 let recovery_address_signature = signatures
441 .get(signer_identity)
442 .cloned()
443 .ok_or(SignatureRequestError::MissingSigner)?;
444
445 Ok(UnverifiedAction::RevokeAssociation(
446 UnverifiedRevokeAssociation {
447 recovery_identifier_signature: recovery_address_signature,
448 unsigned_action,
449 },
450 ))
451 }
452 UnsignedAction::ChangeRecoveryAddress(unsigned_action) => {
453 let signer_identity = pending_action
454 .pending_signatures
455 .get(&SignatureField::RecoveryAddress)
456 .ok_or(SignatureRequestError::MissingSigner)?;
457
458 let recovery_identifier_signature = signatures
459 .get(signer_identity)
460 .cloned()
461 .ok_or(SignatureRequestError::MissingSigner)?;
462
463 Ok(UnverifiedAction::ChangeRecoveryAddress(
464 UnverifiedChangeRecoveryAddress {
465 recovery_identifier_signature,
466 unsigned_action,
467 },
468 ))
469 }
470 UnsignedAction::ClaimHandle(unsigned_action) => {
471 let signer_identity = pending_action
472 .pending_signatures
473 .get(&SignatureField::ExistingMember)
474 .ok_or(SignatureRequestError::MissingSigner)?;
475 let signature = signatures
476 .get(signer_identity)
477 .cloned()
478 .ok_or(SignatureRequestError::MissingSigner)?;
479
480 Ok(UnverifiedAction::ClaimHandle(UnverifiedClaimHandle {
481 unsigned_action,
482 signature,
483 }))
484 }
485 }
486}
487
488fn get_signature_text(
489 actions: Vec<UnsignedAction>,
490 inbox_id: String,
491 client_timestamp_ns: u64,
492) -> String {
493 let identity_update = UnsignedIdentityUpdate {
494 client_timestamp_ns,
495 actions,
496 inbox_id,
497 };
498
499 identity_update.signature_text()
500}
501
502#[cfg(test)]
503pub(crate) mod tests {
504 use alloy::signers::{Signer, local::PrivateKeySigner};
505 use xmtp_cryptography::XmtpInstallationCredential;
506
507 use crate::{
508 InboxOwner,
509 associations::{
510 IdentityUpdate, get_state,
511 test_utils::{
512 MockSmartContractSignatureVerifier, WalletTestExt, add_installation_key_signature,
513 add_wallet_signature,
514 },
515 unverified::UnverifiedRecoverableEcdsaSignature,
516 },
517 };
518
519 use super::*;
520
521 async fn convert_to_verified(identity_update: &UnverifiedIdentityUpdate) -> IdentityUpdate {
522 let scw_verifier = MockSmartContractSignatureVerifier::new(false);
523 identity_update
524 .to_verified(&scw_verifier)
525 .await
526 .expect("should be valid")
527 }
528
529 #[xmtp_common::test]
530 async fn create_inbox() {
531 let wallet = PrivateKeySigner::random();
532 let account_ident = wallet.get_identifier().unwrap();
533 let nonce = 0;
534 let inbox_id = wallet.get_inbox_id(nonce);
535
536 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
537 .create_inbox(account_ident, nonce)
538 .build();
539
540 add_wallet_signature(&mut signature_request, &wallet).await;
541
542 let identity_update = signature_request
543 .build_identity_update()
544 .expect("should be valid");
545
546 get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
547 }
548
549 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
550 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
551 async fn create_and_add_identity() {
552 let wallet = PrivateKeySigner::random();
553 let installation_key = XmtpInstallationCredential::new();
554 let account_address = wallet.get_identifier().unwrap();
555 let nonce = 0;
556 let inbox_id = wallet.get_inbox_id(nonce);
557 let ident = Identifier::eth(&account_address).unwrap();
558 let new_member_identifier =
559 MemberIdentifier::installation(installation_key.public_bytes().to_vec());
560
561 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
562 .create_inbox(ident.clone(), nonce)
563 .add_association(new_member_identifier, ident.into())
564 .build();
565
566 add_wallet_signature(&mut signature_request, &wallet).await;
567 add_installation_key_signature(&mut signature_request, &installation_key).await;
568
569 let identity_update = signature_request
570 .build_identity_update()
571 .expect("should be valid");
572
573 let state =
574 get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
575 assert_eq!(state.members().len(), 2);
576 }
577
578 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
579 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
580 async fn create_and_revoke() {
581 let wallet = PrivateKeySigner::random();
582 let nonce = 0;
583 let inbox_id = wallet.get_inbox_id(nonce);
584 let existing_member_identifier = wallet.identifier();
585
586 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
587 .create_inbox(existing_member_identifier.clone(), nonce)
588 .revoke_association(
589 existing_member_identifier.clone().into(),
590 existing_member_identifier.clone().into(),
591 )
592 .build();
593
594 add_wallet_signature(&mut signature_request, &wallet).await;
595
596 let identity_update = signature_request
597 .build_identity_update()
598 .expect("should be valid");
599
600 let state =
601 get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
602
603 assert_eq!(state.members().len(), 0);
604 }
605
606 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
607 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
608 async fn attempt_adding_unknown_signer() {
609 let account_address = "0x1234567890abcdef1234567890abcdef12345678".to_string();
610 let nonce = 0;
611 let ident = Identifier::eth(&account_address).unwrap();
612 let inbox_id = ident.inbox_id(nonce).unwrap();
613
614 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
615 .create_inbox(ident, nonce)
616 .build();
617
618 let rand_wallet = PrivateKeySigner::random();
619
620 let signature_text = signature_request.signature_text();
621 let sig = rand_wallet
622 .sign_message(signature_text.as_bytes())
623 .await
624 .unwrap();
625 let unverified_sig = UnverifiedSignature::RecoverableEcdsa(
626 UnverifiedRecoverableEcdsaSignature::new(sig.into()),
627 );
628 let scw_verifier = MockSmartContractSignatureVerifier::new(false);
629
630 let attempt_to_add_random_member = signature_request
631 .add_signature(unverified_sig, &scw_verifier)
632 .await;
633
634 assert!(matches!(
635 attempt_to_add_random_member,
636 Err(SignatureRequestError::UnknownSigner)
637 ));
638 }
639
640 fn scw_request(chains: Option<Vec<&str>>) -> (SignatureRequest, String) {
643 let wallet = PrivateKeySigner::random();
644 let account_ident = wallet.get_identifier().unwrap();
645 let inbox_id = wallet.get_inbox_id(0);
646 let mut request = SignatureRequestBuilder::new(inbox_id)
647 .create_inbox(account_ident, 0)
648 .build();
649 if let Some(chains) = chains {
650 let chains: Vec<String> = chains.into_iter().map(str::to_owned).collect();
651 request.restrict_chains(std::sync::Arc::from(chains));
652 }
653 (request, wallet.address().to_string())
654 }
655
656 async fn add_scw_on(
657 request: &mut SignatureRequest,
658 address: &str,
659 chain: &str,
660 ) -> Result<(), SignatureRequestError> {
661 let signature = NewUnverifiedSmartContractWalletSignature::new(
662 vec![1, 2, 3],
663 AccountId::new(chain.to_owned(), address.to_owned()),
664 Some(1),
665 );
666 request
667 .add_new_unverified_smart_contract_signature(
668 signature,
669 &MockSmartContractSignatureVerifier::new(true),
670 )
671 .await
672 }
673
674 #[xmtp_common::test(unwrap_try = true)]
678 async fn a_chain_outside_the_accepted_list_is_refused() {
679 let (mut request, address) = scw_request(Some(vec!["eip155:1", "eip155:8453"]));
680 let error = add_scw_on(&mut request, &address, "eip155:137")
681 .await
682 .unwrap_err();
683 let SignatureRequestError::ChainNotAccepted { chain, accepted } = error else {
684 panic!("expected ChainNotAccepted, got {error}");
685 };
686 assert_eq!(chain, "eip155:137");
687 assert_eq!(accepted, vec!["eip155:1", "eip155:8453"]);
688 assert!(request.signatures.is_empty());
690 }
691
692 #[xmtp_common::test(unwrap_try = true)]
695 async fn a_chain_inside_the_accepted_list_is_admitted() {
696 let (mut request, address) = scw_request(Some(vec!["eip155:1"]));
697 add_scw_on(&mut request, &address, "eip155:1").await?;
698 assert_eq!(request.signatures.len(), 1);
699 }
700
701 #[xmtp_common::test(unwrap_try = true)]
704 async fn an_empty_accepted_list_refuses_every_chain() {
705 let (mut request, address) = scw_request(Some(vec![]));
706 for chain in ["eip155:1", "eip155:8453", "solana:mainnet"] {
707 let error = add_scw_on(&mut request, &address, chain).await.unwrap_err();
708 let SignatureRequestError::ChainNotAccepted { accepted, .. } = error else {
709 panic!("expected ChainNotAccepted for {chain}, got {error}");
710 };
711 assert!(accepted.is_empty());
712 }
713 }
714
715 #[xmtp_common::test(unwrap_try = true)]
718 async fn an_unrestricted_request_accepts_any_chain() {
719 let (mut request, address) = scw_request(None);
720 add_scw_on(&mut request, &address, "eip155:424242").await?;
721 assert_eq!(request.signatures.len(), 1);
722 }
723
724 #[xmtp_common::test(unwrap_try = true)]
729 async fn an_unknown_chain_stays_retryable_during_ordered_processing() {
730 use crate::scw_verifier::{
731 MultiSmartContractSignatureVerifier, SmartContractSignatureVerifier, VerifierError,
732 };
733 use xmtp_common::RetryableError;
734
735 let verifier = MultiSmartContractSignatureVerifier::new(Default::default())?;
736 let Err(error) = verifier
737 .is_valid_signature(
738 AccountId::new(
739 "eip155:424242".to_owned(),
740 PrivateKeySigner::random().address().to_string(),
741 ),
742 [0u8; 32],
743 vec![1, 2, 3].into(),
744 None,
745 )
746 .await
747 else {
748 panic!("a chain with no route must not verify");
749 };
750 assert!(
751 matches!(error, VerifierError::NoVerifier(_)),
752 "ordered processing must see a missing route, got {error}"
753 );
754 assert!(error.is_retryable(), "a missing route retries");
755 }
756}