xmtp_id/scw_verifier/
mod.rs

1mod chain_rpc_verifier;
2mod remote_signature_verifier;
3use crate::associations::AccountId;
4use alloy::{
5    primitives::{BlockNumber, Bytes},
6    providers::DynProvider,
7};
8pub use chain_rpc_verifier::*;
9pub use remote_signature_verifier::*;
10use std::{collections::HashMap, fs, path::Path, sync::Arc};
11use thiserror::Error;
12use tracing::info;
13use url::Url;
14use xmtp_common::{MaybeSend, MaybeSync, RetryableError};
15
16static DEFAULT_CHAIN_URLS: &str = include_str!("chain_urls_default.json");
17
18#[derive(Debug, Error)]
19pub enum VerifierError {
20    #[error("unexpected result from ERC-6492 {0}")]
21    UnexpectedERC6492Result(String),
22    #[error(transparent)]
23    FromHex(#[from] hex::FromHexError),
24    #[error(transparent)]
25    Provider(#[from] alloy::transports::RpcError<alloy::transports::TransportErrorKind>),
26    #[error(transparent)]
27    Url(#[from] url::ParseError),
28    #[error(transparent)]
29    Io(#[from] std::io::Error),
30    #[error(transparent)]
31    Serde(#[from] serde_json::Error),
32    #[error("URLs must be preceded with eip144:")]
33    MalformedEipUrl,
34    #[error("verifier not present")]
35    NoVerifier,
36    #[error("hash was invalid length or otherwise malformed")]
37    InvalidHash(Vec<u8>),
38    #[error("{0}")]
39    Other(Box<dyn RetryableError>),
40}
41
42impl RetryableError for VerifierError {
43    fn is_retryable(&self) -> bool {
44        use VerifierError::*;
45        match self {
46            Io(_) => true,
47            NoVerifier => true,
48            Provider(_) => true,
49            Other(o) => o.is_retryable(),
50            _ => false,
51        }
52    }
53}
54
55#[xmtp_common::async_trait]
56pub trait SmartContractSignatureVerifier: MaybeSend + MaybeSync {
57    /// Verifies an ERC-6492<https://eips.ethereum.org/EIPS/eip-6492> signature.
58    ///
59    /// # Arguments
60    ///
61    /// * `signer` - can be the smart wallet address or EOA address.
62    /// * `hash` - Message digest for the signature.
63    /// * `signature` - Could be encoded smart wallet signature or raw ECDSA signature.
64    async fn is_valid_signature(
65        &self,
66        account_id: AccountId,
67        hash: [u8; 32],
68        signature: Bytes,
69        block_number: Option<BlockNumber>,
70    ) -> Result<ValidationResponse, VerifierError>;
71}
72
73#[xmtp_common::async_trait]
74impl<T> SmartContractSignatureVerifier for Arc<T>
75where
76    T: SmartContractSignatureVerifier,
77{
78    async fn is_valid_signature(
79        &self,
80        account_id: AccountId,
81        hash: [u8; 32],
82        signature: Bytes,
83        block_number: Option<BlockNumber>,
84    ) -> Result<ValidationResponse, VerifierError> {
85        (**self)
86            .is_valid_signature(account_id, hash, signature, block_number)
87            .await
88    }
89}
90
91#[xmtp_common::async_trait]
92impl<T> SmartContractSignatureVerifier for &T
93where
94    T: SmartContractSignatureVerifier,
95{
96    async fn is_valid_signature(
97        &self,
98        account_id: AccountId,
99        hash: [u8; 32],
100        signature: Bytes,
101        block_number: Option<BlockNumber>,
102    ) -> Result<ValidationResponse, VerifierError> {
103        (*self)
104            .is_valid_signature(account_id, hash, signature, block_number)
105            .await
106    }
107}
108
109#[xmtp_common::async_trait]
110impl<T> SmartContractSignatureVerifier for Box<T>
111where
112    T: SmartContractSignatureVerifier + ?Sized,
113{
114    async fn is_valid_signature(
115        &self,
116        account_id: AccountId,
117        hash: [u8; 32],
118        signature: Bytes,
119        block_number: Option<BlockNumber>,
120    ) -> Result<ValidationResponse, VerifierError> {
121        (**self)
122            .is_valid_signature(account_id, hash, signature, block_number)
123            .await
124    }
125}
126
127#[derive(Clone)]
128pub struct ValidationResponse {
129    pub is_valid: bool,
130    pub block_number: Option<u64>,
131    pub error: Option<String>,
132}
133
134pub struct MultiSmartContractSignatureVerifier {
135    verifiers: HashMap<String, Box<dyn SmartContractSignatureVerifier>>,
136}
137
138impl MultiSmartContractSignatureVerifier {
139    pub fn new(urls: HashMap<String, url::Url>) -> Result<Self, VerifierError> {
140        let verifiers = urls
141            .into_iter()
142            .map(|(chain_id, url)| {
143                Ok::<_, VerifierError>((
144                    chain_id,
145                    Box::new(RpcSmartContractWalletVerifier::new(url.to_string())?) as Box<_>,
146                ))
147            })
148            .collect::<Result<HashMap<_, _>, _>>()?;
149
150        Ok(Self { verifiers })
151    }
152
153    pub fn new_providers(providers: HashMap<String, DynProvider>) -> Result<Self, VerifierError> {
154        let verifiers = providers
155            .into_iter()
156            .map(|(chain_id, provider)| {
157                (
158                    chain_id,
159                    Box::new(RpcSmartContractWalletVerifier::new_from_provider(provider)) as Box<_>,
160                )
161            })
162            .collect();
163        Ok(Self { verifiers })
164    }
165
166    pub fn new_from_env() -> Result<Self, VerifierError> {
167        let urls: HashMap<String, Url> = serde_json::from_str(DEFAULT_CHAIN_URLS)?;
168        Self::new(urls)?.upgrade()
169    }
170
171    pub fn new_from_file(path: impl AsRef<Path>) -> Result<Self, VerifierError> {
172        let json = fs::read_to_string(path.as_ref())?;
173        let urls: HashMap<String, Url> = serde_json::from_str(&json)?;
174
175        Self::new(urls)
176    }
177
178    /// Upgrade the default urls to paid/private/alternative urls if the env vars are present.
179    pub fn upgrade(mut self) -> Result<Self, VerifierError> {
180        for (id, verifier) in self.verifiers.iter_mut() {
181            // TODO: coda - update the chain id env var ids to preceded with "EIP155_"
182            let eip_id = id.split(":").nth(1).ok_or(VerifierError::MalformedEipUrl)?;
183            if let Ok(url) = std::env::var(format!("CHAIN_RPC_{eip_id}")) {
184                *verifier = Box::new(RpcSmartContractWalletVerifier::new(url)?);
185            } else {
186                info!("No upgraded chain url for chain {id}, using default.");
187            };
188        }
189
190        #[cfg(feature = "test-utils")]
191        if let Ok(url) = std::env::var("ANVIL_URL") {
192            info!("Adding anvil from env to the verifiers: {url}");
193            self.add_anvil(url)?;
194        } else {
195            use xmtp_configuration::DockerUrls;
196            info!("adding default anvil url @{}", DockerUrls::ANVIL);
197            self.add_anvil(DockerUrls::ANVIL.to_string())?;
198        }
199        Ok(self)
200    }
201
202    pub fn add_verifier(&mut self, id: String, url: String) -> Result<(), VerifierError> {
203        self.verifiers
204            .insert(id, Box::new(RpcSmartContractWalletVerifier::new(url)?));
205        Ok(())
206    }
207
208    pub fn add_anvil(&mut self, url: String) -> Result<(), VerifierError> {
209        self.verifiers.insert(
210            "eip155:31337".to_string(),
211            Box::new(RpcSmartContractWalletVerifier::new(url)?),
212        );
213        Ok(())
214    }
215}
216
217#[xmtp_common::async_trait]
218impl SmartContractSignatureVerifier for MultiSmartContractSignatureVerifier {
219    async fn is_valid_signature(
220        &self,
221        account_id: AccountId,
222        hash: [u8; 32],
223        signature: Bytes,
224        block_number: Option<BlockNumber>,
225    ) -> Result<ValidationResponse, VerifierError> {
226        if let Some(verifier) = self.verifiers.get(&account_id.chain_id) {
227            return verifier
228                .is_valid_signature(account_id, hash, signature, block_number)
229                .await;
230        }
231
232        Err(VerifierError::NoVerifier)
233    }
234}