Skip to main content

xmtp_id/associations/
builder.rs

1//! Builders for creating a [`SignatureRequest`] with a [`PendingIdentityAction`] for an external SDK/Library, which can then be
2//! resolved into an [`IdentityUpdate`](super::association_log::IdentityUpdate). An [`IdentityUpdate`](super::association_log::IdentityUpdate) may be used for updating the state
3//! of an XMTP ID according to [XIP-46](https://github.com/xmtp/XIPs/pull/53)
4
5use 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/// The SignatureField is used to map the signatures from a [SignatureRequest] back to the correct
30/// field in an [IdentityUpdate]. It is used in the `pending_signatures` map in a [PendingIdentityAction]
31#[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
45/// The SignatureRequestBuilder is used to collect all of the actions in
46/// an IdentityUpdate, but without the signatures.
47/// It outputs a SignatureRequest, which can then collect the relevant signatures and be turned into
48/// an IdentityUpdate.
49pub struct SignatureRequestBuilder {
50    inbox_id: String,
51    client_timestamp_ns: u64,
52    actions: Vec<PendingIdentityAction>,
53}
54
55impl SignatureRequestBuilder {
56    /// Create a new IdentityUpdateBuilder for the given `inbox_id`
57    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    /// Create a new inbox. This method must be called before any other methods or the IdentityUpdate will fail
66    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        // Save the `PendingIdentityAction` for later
78        self.actions.push(pending_action);
79
80        self
81    }
82
83    /// Add an AddAssociation action.
84    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    /// Add a ClaimHandle action signed by `signer`, a current member of the
139    /// inbox. A claim must be the only action of its update, so a request with
140    /// a claim and any other action builds an update that no validator applies.
141    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    /// Unknown signer.
177    ///
178    /// Signer not recognized for this request. Not retryable.
179    #[error("Unknown signer")]
180    UnknownSigner,
181    /// Missing signer.
182    ///
183    /// Required signature was not provided. Not retryable.
184    #[error("Required signature was not provided")]
185    MissingSigner,
186    #[error("Signature error {0}")]
187    #[error_code(inherit)]
188    Signature(#[from] SignatureError),
189    /// Unable to get block number.
190    ///
191    /// Block number not returned after successful SCW verification. May be retryable.
192    #[error("Unable to get block number")]
193    BlockNumber,
194    /// The deployment does not accept this chain.
195    ///
196    /// The smart contract wallet signature names a chain outside the list the
197    /// backend published. Not retryable.
198    #[error("the backend does not accept chain {chain}; it accepts {accepted:?}")]
199    ChainNotAccepted {
200        chain: String,
201        accepted: Vec<String>,
202    },
203}
204
205/// A signature request is meant to be sent over the FFI barrier (wrapped in a mutex) to platform SDKs.
206/// `xmtp_mls` can add any InstallationKey signatures first, so that the platform SDK does not need to worry about those.
207/// The platform SDK can then fill in any missing signatures and convert it to an IdentityUpdate that is ready to be published
208/// to the network
209#[derive(Clone, Debug)]
210pub struct SignatureRequest {
211    pending_actions: Vec<PendingIdentityAction>,
212    signature_text: String,
213    signatures: HashMap<MemberIdentifier, UnverifiedSignature>,
214    /// The chains app-supplied smart contract wallet signatures may name
215    /// explicitly. `None` restricts nothing, which is what a request
216    /// built without a client — or by a client whose app supplied its own
217    /// verifier — keeps.
218    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    /// Often the front-end doesn't know the current block number when adding a smart contract.
256    /// This is for when you want to add a smart-contract wallet,
257    /// and need the verifier to populate the latest block number for you.
258    /// Restrict app-supplied smart contract wallet signatures to these chains
259    /// selected by the client from the snapshot it resolved,
260    /// before the request is handed to the app.
261    pub fn restrict_chains(&mut self, chains: std::sync::Arc<[String]>) {
262        self.accepted_chains = Some(chains);
263    }
264
265    /// The chains this request was restricted to, or `None` when nothing
266    /// restricted it. `Some(&[])` is a deployment that accepts no chain
267    /// at all, which is not the same as no restriction.
268    pub fn accepted_chains(&self) -> Option<&[String]> {
269        self.accepted_chains.as_deref()
270    }
271
272    /// Refuse a chain the deployment does not accept
273    /// before the verifier reaches the network. An empty accepted list refuses
274    /// every chain.
275    // implements: CONF-046
276    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        // Make sure the signer is someone actually in the request
348        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    /// A request whose one pending member is a smart contract wallet, with the
641    /// wallet's address alongside it.
642    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    // A chain outside the published list is refused, and
675    // the error names both the chain and the list.
676    // verifies: CONF-046
677    #[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        // Nothing was verified, so nothing reached the signature set.
689        assert!(request.signatures.is_empty());
690    }
691
692    // A chain the deployment named is accepted, so the restriction is
693    // the list and not a blanket refusal.
694    #[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    // An empty list refuses every chain.
702    // verifies: CONF-046
703    #[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    // A request no client restricted — one built without a client, or
716    // by a client whose app supplied its own verifier — restricts nothing.
717    #[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    // Ordered processing never sees this check. It
725    // verifies an identity update that is already on the network, where a chain
726    // with no route is the retryable `NoVerifier`, not `ChainNotAccepted`.
727    // verifies: IDENT-061
728    #[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}