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, MemberIdentifier, MemberKind, SignatureError,
15    unsigned_actions::{
16        SignatureTextCreator, UnsignedAction, UnsignedAddAssociation,
17        UnsignedChangeRecoveryAddress, UnsignedCreateInbox, UnsignedIdentityUpdate,
18        UnsignedRevokeAssociation,
19    },
20    unverified::{
21        NewUnverifiedSmartContractWalletSignature, UnverifiedAction, UnverifiedAddAssociation,
22        UnverifiedChangeRecoveryAddress, UnverifiedCreateInbox, UnverifiedIdentityUpdate,
23        UnverifiedRevokeAssociation, UnverifiedSignature, UnverifiedSmartContractWalletSignature,
24    },
25    verified_signature::VerifiedSignature,
26};
27
28/// The SignatureField is used to map the signatures from a [SignatureRequest] back to the correct
29/// field in an [IdentityUpdate]. It is used in the `pending_signatures` map in a [PendingIdentityAction]
30#[derive(Clone, PartialEq, Hash, Eq, Debug)]
31enum SignatureField {
32    InitialAddress,
33    ExistingMember,
34    NewMember,
35    RecoveryAddress,
36}
37
38#[derive(Clone, Debug)]
39pub struct PendingIdentityAction {
40    unsigned_action: UnsignedAction,
41    pending_signatures: HashMap<SignatureField, MemberIdentifier>,
42}
43
44/// The SignatureRequestBuilder is used to collect all of the actions in
45/// an IdentityUpdate, but without the signatures.
46/// It outputs a SignatureRequest, which can then collect the relevant signatures and be turned into
47/// an IdentityUpdate.
48pub struct SignatureRequestBuilder {
49    inbox_id: String,
50    client_timestamp_ns: u64,
51    actions: Vec<PendingIdentityAction>,
52}
53
54impl SignatureRequestBuilder {
55    /// Create a new IdentityUpdateBuilder for the given `inbox_id`
56    pub fn new<S: AsRef<str>>(inbox_id: S) -> Self {
57        Self {
58            inbox_id: inbox_id.as_ref().to_string(),
59            client_timestamp_ns: now_ns() as u64,
60            actions: vec![],
61        }
62    }
63
64    /// Create a new inbox. This method must be called before any other methods or the IdentityUpdate will fail
65    pub fn create_inbox(mut self, signer_identity: Identifier, nonce: u64) -> Self {
66        let pending_action = PendingIdentityAction {
67            unsigned_action: UnsignedAction::CreateInbox(UnsignedCreateInbox {
68                account_identifier: signer_identity.clone(),
69                nonce,
70            }),
71            pending_signatures: HashMap::from([(
72                SignatureField::InitialAddress,
73                signer_identity.into(),
74            )]),
75        };
76        // Save the `PendingIdentityAction` for later
77        self.actions.push(pending_action);
78
79        self
80    }
81
82    /// Add an AddAssociation action.
83    pub fn add_association(
84        mut self,
85        new_member_identifier: MemberIdentifier,
86        existing_member_identifier: MemberIdentifier,
87    ) -> Self {
88        self.actions.push(PendingIdentityAction {
89            unsigned_action: UnsignedAction::AddAssociation(UnsignedAddAssociation {
90                new_member_identifier: new_member_identifier.clone(),
91            }),
92            pending_signatures: HashMap::from([
93                (SignatureField::ExistingMember, existing_member_identifier),
94                (SignatureField::NewMember, new_member_identifier),
95            ]),
96        });
97
98        self
99    }
100
101    pub fn revoke_association(
102        mut self,
103        recovery_address_signer: MemberIdentifier,
104        revoked_member: MemberIdentifier,
105    ) -> Self {
106        self.actions.push(PendingIdentityAction {
107            pending_signatures: HashMap::from([(
108                SignatureField::RecoveryAddress,
109                recovery_address_signer,
110            )]),
111            unsigned_action: UnsignedAction::RevokeAssociation(UnsignedRevokeAssociation {
112                revoked_member,
113            }),
114        });
115
116        self
117    }
118
119    pub fn change_recovery_address(
120        mut self,
121        recovery_address_signer: MemberIdentifier,
122        new_recovery_identifier: Identifier,
123    ) -> Self {
124        self.actions.push(PendingIdentityAction {
125            pending_signatures: HashMap::from([(
126                SignatureField::RecoveryAddress,
127                recovery_address_signer,
128            )]),
129            unsigned_action: UnsignedAction::ChangeRecoveryAddress(UnsignedChangeRecoveryAddress {
130                new_recovery_identifier,
131            }),
132        });
133
134        self
135    }
136
137    pub fn build(self) -> SignatureRequest {
138        let unsigned_actions: Vec<UnsignedAction> = self
139            .actions
140            .iter()
141            .map(|pending_action| pending_action.unsigned_action.clone())
142            .collect();
143
144        let signature_text = get_signature_text(
145            unsigned_actions,
146            self.inbox_id.clone(),
147            self.client_timestamp_ns,
148        );
149
150        SignatureRequest::new(
151            self.actions,
152            signature_text,
153            self.inbox_id,
154            self.client_timestamp_ns,
155        )
156    }
157}
158
159#[derive(Debug, Error, ErrorCode)]
160pub enum SignatureRequestError {
161    /// Unknown signer.
162    ///
163    /// Signer not recognized for this request. Not retryable.
164    #[error("Unknown signer")]
165    UnknownSigner,
166    /// Missing signer.
167    ///
168    /// Required signature was not provided. Not retryable.
169    #[error("Required signature was not provided")]
170    MissingSigner,
171    #[error("Signature error {0}")]
172    #[error_code(inherit)]
173    Signature(#[from] SignatureError),
174    /// Unable to get block number.
175    ///
176    /// Block number not returned after successful SCW verification. May be retryable.
177    #[error("Unable to get block number")]
178    BlockNumber,
179    /// The deployment does not accept this chain.
180    ///
181    /// The smart contract wallet signature names a chain outside the list the
182    /// backend published (CFG-069, CFG-070). Not retryable.
183    #[error("the backend does not accept chain {chain}; it accepts {accepted:?}")]
184    ChainNotAccepted {
185        chain: String,
186        accepted: Vec<String>,
187    },
188}
189
190/// A signature request is meant to be sent over the FFI barrier (wrapped in a mutex) to platform SDKs.
191/// `xmtp_mls` can add any InstallationKey signatures first, so that the platform SDK does not need to worry about those.
192/// The platform SDK can then fill in any missing signatures and convert it to an IdentityUpdate that is ready to be published
193/// to the network
194#[derive(Clone, Debug)]
195pub struct SignatureRequest {
196    pending_actions: Vec<PendingIdentityAction>,
197    signature_text: String,
198    signatures: HashMap<MemberIdentifier, UnverifiedSignature>,
199    /// The chains app-supplied smart contract wallet signatures may name
200    /// (CFG-069, CFG-070). `None` restricts nothing, which is what a request
201    /// built without a client — or by a client whose app supplied its own
202    /// verifier — keeps.
203    accepted_chains: Option<std::sync::Arc<[String]>>,
204    client_timestamp_ns: u64,
205    inbox_id: String,
206}
207
208impl SignatureRequest {
209    pub fn new(
210        pending_actions: Vec<PendingIdentityAction>,
211        signature_text: String,
212        inbox_id: String,
213        client_timestamp_ns: u64,
214    ) -> Self {
215        Self {
216            inbox_id,
217            pending_actions,
218            signature_text,
219            signatures: HashMap::new(),
220            accepted_chains: None,
221            client_timestamp_ns,
222        }
223    }
224
225    pub fn missing_signatures(&self) -> Vec<&MemberIdentifier> {
226        self.pending_actions
227            .iter()
228            .flat_map(|pending_action| pending_action.pending_signatures.values())
229            .filter(|ident| !self.signatures.contains_key(ident))
230            .collect()
231    }
232
233    pub fn missing_address_signatures(&self) -> Vec<&MemberIdentifier> {
234        self.missing_signatures()
235            .into_iter()
236            .filter(|member| matches!(member.kind(), MemberKind::Ethereum | MemberKind::Passkey))
237            .collect()
238    }
239
240    /// Often the front-end doesn't know the current block number when adding a smart contract.
241    /// This is for when you want to add a smart-contract wallet,
242    /// and need the verifier to populate the latest block number for you.
243    /// Restrict app-supplied smart contract wallet signatures to these chains
244    /// (CFG-069, CFG-070). Set by the client from the snapshot it resolved,
245    /// before the request is handed to the app.
246    pub fn restrict_chains(&mut self, chains: std::sync::Arc<[String]>) {
247        self.accepted_chains = Some(chains);
248    }
249
250    /// The chains this request was restricted to, or `None` when nothing
251    /// restricted it. `Some(&[])` is a deployment that accepts no chain
252    /// (CFG-070), which is not the same as no restriction.
253    pub fn accepted_chains(&self) -> Option<&[String]> {
254        self.accepted_chains.as_deref()
255    }
256
257    /// CFG-069 and CFG-070: refuse a chain the deployment does not accept
258    /// before the verifier reaches the network. An empty accepted list refuses
259    /// every chain.
260    fn check_chain(&self, account_id: &AccountId) -> Result<(), SignatureRequestError> {
261        let Some(accepted) = self.accepted_chains.as_ref() else {
262            return Ok(());
263        };
264        let chain = account_id.get_chain_id();
265        if accepted.iter().any(|accepted| accepted == chain) {
266            return Ok(());
267        }
268        Err(SignatureRequestError::ChainNotAccepted {
269            chain: chain.to_string(),
270            accepted: accepted.to_vec(),
271        })
272    }
273
274    pub async fn add_new_unverified_smart_contract_signature(
275        &mut self,
276        mut signature: NewUnverifiedSmartContractWalletSignature,
277        scw_verifier: impl SmartContractSignatureVerifier,
278    ) -> Result<(), SignatureRequestError> {
279        self.check_chain(&signature.account_id)?;
280        let verified_signature = VerifiedSignature::from_smart_contract_wallet(
281            &self.signature_text,
282            scw_verifier,
283            &signature.signature_bytes,
284            signature.account_id.clone(),
285            &mut signature.block_number,
286        )
287        .await?;
288
289        let Some(block_number) = signature.block_number else {
290            return Err(SignatureRequestError::BlockNumber);
291        };
292
293        self.add_verified_signature(
294            UnverifiedSignature::SmartContractWallet(UnverifiedSmartContractWalletSignature {
295                account_id: signature.account_id,
296                block_number,
297                signature_bytes: signature.signature_bytes,
298            }),
299            verified_signature,
300        )
301    }
302
303    pub async fn add_signature(
304        &mut self,
305        signature: UnverifiedSignature,
306        scw_verifier: impl SmartContractSignatureVerifier,
307    ) -> Result<(), SignatureRequestError> {
308        if let UnverifiedSignature::SmartContractWallet(scw) = &signature {
309            self.check_chain(&scw.account_id)?;
310        }
311        let verified_signature = signature
312            .to_verified(self.signature_text.clone(), scw_verifier)
313            .await?;
314
315        self.add_verified_signature(signature, verified_signature)
316    }
317
318    fn add_verified_signature(
319        &mut self,
320        signature: UnverifiedSignature,
321        verified_signature: VerifiedSignature,
322    ) -> Result<(), SignatureRequestError> {
323        let signer_identity = &verified_signature.signer;
324
325        let missing_signatures = self.missing_signatures();
326        tracing::info!(
327            signer = %signer_identity,
328            missing_signatures=?missing_signatures,
329            "adding verified signature");
330
331        // Make sure the signer is someone actually in the request
332        if !missing_signatures.contains(&signer_identity) {
333            return Err(SignatureRequestError::UnknownSigner);
334        }
335
336        self.signatures.insert(verified_signature.signer, signature);
337
338        Ok(())
339    }
340
341    pub fn is_ready(&self) -> bool {
342        self.missing_signatures().is_empty()
343    }
344
345    pub fn signature_text(&self) -> String {
346        self.signature_text.clone()
347    }
348
349    pub fn build_identity_update(self) -> Result<UnverifiedIdentityUpdate, SignatureRequestError> {
350        if !self.is_ready() {
351            return Err(SignatureRequestError::MissingSigner);
352        }
353
354        let actions = self
355            .pending_actions
356            .clone()
357            .into_iter()
358            .map(|pending_action| build_action(pending_action, &self.signatures))
359            .collect::<Result<Vec<UnverifiedAction>, SignatureRequestError>>()?;
360
361        Ok(UnverifiedIdentityUpdate::new(
362            self.inbox_id,
363            self.client_timestamp_ns,
364            actions,
365        ))
366    }
367
368    pub fn inbox_id(&self) -> crate::InboxIdRef<'_> {
369        &self.inbox_id
370    }
371}
372
373fn build_action(
374    pending_action: PendingIdentityAction,
375    signatures: &HashMap<MemberIdentifier, UnverifiedSignature>,
376) -> Result<UnverifiedAction, SignatureRequestError> {
377    match pending_action.unsigned_action {
378        UnsignedAction::CreateInbox(unsigned_action) => {
379            let signer_identity = pending_action
380                .pending_signatures
381                .get(&SignatureField::InitialAddress)
382                .ok_or(SignatureRequestError::MissingSigner)?;
383            let initial_identifier_signature = signatures
384                .get(signer_identity)
385                .cloned()
386                .ok_or(SignatureRequestError::MissingSigner)?;
387
388            Ok(UnverifiedAction::CreateInbox(UnverifiedCreateInbox {
389                unsigned_action,
390                initial_identifier_signature,
391            }))
392        }
393        UnsignedAction::AddAssociation(unsigned_action) => {
394            let existing_member_signer_identity = pending_action
395                .pending_signatures
396                .get(&SignatureField::ExistingMember)
397                .ok_or(SignatureRequestError::MissingSigner)?;
398            let new_member_signer_identity = pending_action
399                .pending_signatures
400                .get(&SignatureField::NewMember)
401                .ok_or(SignatureRequestError::MissingSigner)?;
402
403            let existing_member_signature = signatures
404                .get(existing_member_signer_identity)
405                .cloned()
406                .ok_or(SignatureRequestError::MissingSigner)?;
407
408            let new_member_signature = signatures
409                .get(new_member_signer_identity)
410                .cloned()
411                .ok_or(SignatureRequestError::MissingSigner)?;
412
413            Ok(UnverifiedAction::AddAssociation(UnverifiedAddAssociation {
414                unsigned_action,
415                existing_member_signature,
416                new_member_signature,
417            }))
418        }
419        UnsignedAction::RevokeAssociation(unsigned_action) => {
420            let signer_identity = pending_action
421                .pending_signatures
422                .get(&SignatureField::RecoveryAddress)
423                .ok_or(SignatureRequestError::MissingSigner)?;
424            let recovery_address_signature = signatures
425                .get(signer_identity)
426                .cloned()
427                .ok_or(SignatureRequestError::MissingSigner)?;
428
429            Ok(UnverifiedAction::RevokeAssociation(
430                UnverifiedRevokeAssociation {
431                    recovery_identifier_signature: recovery_address_signature,
432                    unsigned_action,
433                },
434            ))
435        }
436        UnsignedAction::ChangeRecoveryAddress(unsigned_action) => {
437            let signer_identity = pending_action
438                .pending_signatures
439                .get(&SignatureField::RecoveryAddress)
440                .ok_or(SignatureRequestError::MissingSigner)?;
441
442            let recovery_identifier_signature = signatures
443                .get(signer_identity)
444                .cloned()
445                .ok_or(SignatureRequestError::MissingSigner)?;
446
447            Ok(UnverifiedAction::ChangeRecoveryAddress(
448                UnverifiedChangeRecoveryAddress {
449                    recovery_identifier_signature,
450                    unsigned_action,
451                },
452            ))
453        }
454    }
455}
456
457fn get_signature_text(
458    actions: Vec<UnsignedAction>,
459    inbox_id: String,
460    client_timestamp_ns: u64,
461) -> String {
462    let identity_update = UnsignedIdentityUpdate {
463        client_timestamp_ns,
464        actions,
465        inbox_id,
466    };
467
468    identity_update.signature_text()
469}
470
471#[cfg(test)]
472pub(crate) mod tests {
473    use alloy::signers::{Signer, local::PrivateKeySigner};
474    use xmtp_cryptography::XmtpInstallationCredential;
475
476    use crate::{
477        InboxOwner,
478        associations::{
479            IdentityUpdate, get_state,
480            test_utils::{
481                MockSmartContractSignatureVerifier, WalletTestExt, add_installation_key_signature,
482                add_wallet_signature,
483            },
484            unverified::UnverifiedRecoverableEcdsaSignature,
485        },
486    };
487
488    use super::*;
489
490    async fn convert_to_verified(identity_update: &UnverifiedIdentityUpdate) -> IdentityUpdate {
491        let scw_verifier = MockSmartContractSignatureVerifier::new(false);
492        identity_update
493            .to_verified(&scw_verifier)
494            .await
495            .expect("should be valid")
496    }
497
498    #[xmtp_common::test]
499    async fn create_inbox() {
500        let wallet = PrivateKeySigner::random();
501        let account_ident = wallet.get_identifier().unwrap();
502        let nonce = 0;
503        let inbox_id = wallet.get_inbox_id(nonce);
504
505        let mut signature_request = SignatureRequestBuilder::new(inbox_id)
506            .create_inbox(account_ident, nonce)
507            .build();
508
509        add_wallet_signature(&mut signature_request, &wallet).await;
510
511        let identity_update = signature_request
512            .build_identity_update()
513            .expect("should be valid");
514
515        get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
516    }
517
518    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
519    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
520    async fn create_and_add_identity() {
521        let wallet = PrivateKeySigner::random();
522        let installation_key = XmtpInstallationCredential::new();
523        let account_address = wallet.get_identifier().unwrap();
524        let nonce = 0;
525        let inbox_id = wallet.get_inbox_id(nonce);
526        let ident = Identifier::eth(&account_address).unwrap();
527        let new_member_identifier =
528            MemberIdentifier::installation(installation_key.public_bytes().to_vec());
529
530        let mut signature_request = SignatureRequestBuilder::new(inbox_id)
531            .create_inbox(ident.clone(), nonce)
532            .add_association(new_member_identifier, ident.into())
533            .build();
534
535        add_wallet_signature(&mut signature_request, &wallet).await;
536        add_installation_key_signature(&mut signature_request, &installation_key).await;
537
538        let identity_update = signature_request
539            .build_identity_update()
540            .expect("should be valid");
541
542        let state =
543            get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
544        assert_eq!(state.members().len(), 2);
545    }
546
547    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
548    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
549    async fn create_and_revoke() {
550        let wallet = PrivateKeySigner::random();
551        let nonce = 0;
552        let inbox_id = wallet.get_inbox_id(nonce);
553        let existing_member_identifier = wallet.identifier();
554
555        let mut signature_request = SignatureRequestBuilder::new(inbox_id)
556            .create_inbox(existing_member_identifier.clone(), nonce)
557            .revoke_association(
558                existing_member_identifier.clone().into(),
559                existing_member_identifier.clone().into(),
560            )
561            .build();
562
563        add_wallet_signature(&mut signature_request, &wallet).await;
564
565        let identity_update = signature_request
566            .build_identity_update()
567            .expect("should be valid");
568
569        let state =
570            get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
571
572        assert_eq!(state.members().len(), 0);
573    }
574
575    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
576    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
577    async fn attempt_adding_unknown_signer() {
578        let account_address = "0x1234567890abcdef1234567890abcdef12345678".to_string();
579        let nonce = 0;
580        let ident = Identifier::eth(&account_address).unwrap();
581        let inbox_id = ident.inbox_id(nonce).unwrap();
582
583        let mut signature_request = SignatureRequestBuilder::new(inbox_id)
584            .create_inbox(ident, nonce)
585            .build();
586
587        let rand_wallet = PrivateKeySigner::random();
588
589        let signature_text = signature_request.signature_text();
590        let sig = rand_wallet
591            .sign_message(signature_text.as_bytes())
592            .await
593            .unwrap();
594        let unverified_sig = UnverifiedSignature::RecoverableEcdsa(
595            UnverifiedRecoverableEcdsaSignature::new(sig.into()),
596        );
597        let scw_verifier = MockSmartContractSignatureVerifier::new(false);
598
599        let attempt_to_add_random_member = signature_request
600            .add_signature(unverified_sig, &scw_verifier)
601            .await;
602
603        assert!(matches!(
604            attempt_to_add_random_member,
605            Err(SignatureRequestError::UnknownSigner)
606        ));
607    }
608
609    /// A request whose one pending member is a smart contract wallet, with the
610    /// wallet's address alongside it.
611    fn scw_request(chains: Option<Vec<&str>>) -> (SignatureRequest, String) {
612        let wallet = PrivateKeySigner::random();
613        let account_ident = wallet.get_identifier().unwrap();
614        let inbox_id = wallet.get_inbox_id(0);
615        let mut request = SignatureRequestBuilder::new(inbox_id)
616            .create_inbox(account_ident, 0)
617            .build();
618        if let Some(chains) = chains {
619            let chains: Vec<String> = chains.into_iter().map(str::to_owned).collect();
620            request.restrict_chains(std::sync::Arc::from(chains));
621        }
622        (request, wallet.address().to_string())
623    }
624
625    async fn add_scw_on(
626        request: &mut SignatureRequest,
627        address: &str,
628        chain: &str,
629    ) -> Result<(), SignatureRequestError> {
630        let signature = NewUnverifiedSmartContractWalletSignature::new(
631            vec![1, 2, 3],
632            AccountId::new(chain.to_owned(), address.to_owned()),
633            Some(1),
634        );
635        request
636            .add_new_unverified_smart_contract_signature(
637                signature,
638                &MockSmartContractSignatureVerifier::new(true),
639            )
640            .await
641    }
642
643    // CFG-069 and CFG-105: a chain outside the published list is refused, and
644    // the error names both the chain and the list.
645    #[xmtp_common::test(unwrap_try = true)]
646    async fn a_chain_outside_the_accepted_list_is_refused() {
647        let (mut request, address) = scw_request(Some(vec!["eip155:1", "eip155:8453"]));
648        let error = add_scw_on(&mut request, &address, "eip155:137")
649            .await
650            .unwrap_err();
651        let SignatureRequestError::ChainNotAccepted { chain, accepted } = error else {
652            panic!("expected ChainNotAccepted, got {error}");
653        };
654        assert_eq!(chain, "eip155:137");
655        assert_eq!(accepted, vec!["eip155:1", "eip155:8453"]);
656        // Nothing was verified, so nothing reached the signature set.
657        assert!(request.signatures.is_empty());
658    }
659
660    // CFG-069: a chain the deployment named is accepted, so the restriction is
661    // the list and not a blanket refusal.
662    #[xmtp_common::test(unwrap_try = true)]
663    async fn a_chain_inside_the_accepted_list_is_admitted() {
664        let (mut request, address) = scw_request(Some(vec!["eip155:1"]));
665        add_scw_on(&mut request, &address, "eip155:1").await?;
666        assert_eq!(request.signatures.len(), 1);
667    }
668
669    // CFG-070 and CFG-105: an empty list refuses every chain.
670    #[xmtp_common::test(unwrap_try = true)]
671    async fn an_empty_accepted_list_refuses_every_chain() {
672        let (mut request, address) = scw_request(Some(vec![]));
673        for chain in ["eip155:1", "eip155:8453", "solana:mainnet"] {
674            let error = add_scw_on(&mut request, &address, chain).await.unwrap_err();
675            let SignatureRequestError::ChainNotAccepted { accepted, .. } = error else {
676                panic!("expected ChainNotAccepted for {chain}, got {error}");
677            };
678            assert!(accepted.is_empty());
679        }
680    }
681
682    // CFG-069: a request no client restricted — one built without a client, or
683    // by a client whose app supplied its own verifier — restricts nothing.
684    #[xmtp_common::test(unwrap_try = true)]
685    async fn an_unrestricted_request_accepts_any_chain() {
686        let (mut request, address) = scw_request(None);
687        add_scw_on(&mut request, &address, "eip155:424242").await?;
688        assert_eq!(request.signatures.len(), 1);
689    }
690
691    // CFG-097 and CFG-105: ordered processing never sees this check. It
692    // verifies an identity update that is already on the network, where a chain
693    // with no route is the retryable `NoVerifier`, not `ChainNotAccepted`.
694    #[xmtp_common::test(unwrap_try = true)]
695    async fn an_unknown_chain_stays_retryable_during_ordered_processing() {
696        use crate::scw_verifier::{
697            MultiSmartContractSignatureVerifier, SmartContractSignatureVerifier, VerifierError,
698        };
699        use xmtp_common::RetryableError;
700
701        let verifier = MultiSmartContractSignatureVerifier::new(Default::default())?;
702        let Err(error) = verifier
703            .is_valid_signature(
704                AccountId::new(
705                    "eip155:424242".to_owned(),
706                    PrivateKeySigner::random().address().to_string(),
707                ),
708                [0u8; 32],
709                vec![1, 2, 3].into(),
710                None,
711            )
712            .await
713        else {
714            panic!("a chain with no route must not verify");
715        };
716        assert!(
717            matches!(error, VerifierError::NoVerifier(_)),
718            "ordered processing must see a missing route, got {error}"
719        );
720        assert!(error.is_retryable(), "STR-076: a missing route retries");
721    }
722}