Skip to main content

clear_signing/resolver/
source.rs

1use std::collections::HashMap;
2use std::future::Future;
3use std::pin::Pin;
4
5use crate::error::ResolveError;
6use crate::types::descriptor::Descriptor;
7
8/// A resolved descriptor ready for use.
9#[derive(Debug, Clone)]
10pub struct ResolvedDescriptor {
11    pub descriptor: Descriptor,
12    pub chain_id: u64,
13    pub address: String,
14}
15
16/// Lookup parameters for EIP-712 descriptor resolution.
17#[derive(Debug, Clone)]
18pub struct TypedDescriptorLookup {
19    pub chain_id: u64,
20    pub verifying_contract: String,
21    pub primary_type: String,
22    pub encode_type_hash: Option<String>,
23}
24
25/// Trait for descriptor sources (embedded, filesystem, GitHub API, etc.).
26pub trait DescriptorSource: Send + Sync {
27    /// Resolve a descriptor for contract calldata clear signing.
28    fn resolve_calldata(
29        &self,
30        chain_id: u64,
31        address: &str,
32    ) -> Pin<Box<dyn Future<Output = Result<ResolvedDescriptor, ResolveError>> + Send + '_>>;
33
34    /// Resolve candidate descriptors for EIP-712 typed data clear signing.
35    fn resolve_typed_candidates(
36        &self,
37        lookup: TypedDescriptorLookup,
38    ) -> Pin<Box<dyn Future<Output = Result<Vec<ResolvedDescriptor>, ResolveError>> + Send + '_>>;
39}
40
41/// Static in-memory descriptor source for testing.
42pub struct StaticSource {
43    /// Map of `"{chain_id}:{address}"` → Descriptor.
44    calldata: HashMap<String, Descriptor>,
45    typed: HashMap<String, Vec<Descriptor>>,
46}
47
48impl StaticSource {
49    pub fn new() -> Self {
50        Self {
51            calldata: HashMap::new(),
52            typed: HashMap::new(),
53        }
54    }
55
56    fn make_key(chain_id: u64, address: &str) -> String {
57        format!("{}:{}", chain_id, address.to_lowercase())
58    }
59
60    /// Add a calldata descriptor.
61    pub fn add_calldata(&mut self, chain_id: u64, address: &str, descriptor: Descriptor) {
62        self.calldata
63            .insert(Self::make_key(chain_id, address), descriptor);
64    }
65
66    /// Add a typed data descriptor.
67    pub fn add_typed(&mut self, chain_id: u64, address: &str, descriptor: Descriptor) {
68        self.typed
69            .entry(Self::make_key(chain_id, address))
70            .or_default()
71            .push(descriptor);
72    }
73
74    /// Add a calldata descriptor from JSON.
75    pub fn add_calldata_json(
76        &mut self,
77        chain_id: u64,
78        address: &str,
79        json: &str,
80    ) -> Result<(), ResolveError> {
81        let descriptor: Descriptor =
82            serde_json::from_str(json).map_err(|e| ResolveError::Parse(e.to_string()))?;
83        self.add_calldata(chain_id, address, descriptor);
84        Ok(())
85    }
86
87    /// Add a typed data descriptor from JSON.
88    pub fn add_typed_json(
89        &mut self,
90        chain_id: u64,
91        address: &str,
92        json: &str,
93    ) -> Result<(), ResolveError> {
94        let descriptor: Descriptor =
95            serde_json::from_str(json).map_err(|e| ResolveError::Parse(e.to_string()))?;
96        self.add_typed(chain_id, address, descriptor);
97        Ok(())
98    }
99}
100
101impl Default for StaticSource {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107impl DescriptorSource for StaticSource {
108    fn resolve_calldata(
109        &self,
110        chain_id: u64,
111        address: &str,
112    ) -> Pin<Box<dyn Future<Output = Result<ResolvedDescriptor, ResolveError>> + Send + '_>> {
113        let key = Self::make_key(chain_id, address);
114        let result = self
115            .calldata
116            .get(&key)
117            .cloned()
118            .map(|descriptor| ResolvedDescriptor {
119                descriptor,
120                chain_id,
121                address: address.to_lowercase(),
122            })
123            .ok_or_else(|| ResolveError::NotFound {
124                chain_id,
125                address: address.to_string(),
126            });
127        Box::pin(async move { result })
128    }
129
130    fn resolve_typed_candidates(
131        &self,
132        lookup: TypedDescriptorLookup,
133    ) -> Pin<Box<dyn Future<Output = Result<Vec<ResolvedDescriptor>, ResolveError>> + Send + '_>>
134    {
135        let key = Self::make_key(lookup.chain_id, &lookup.verifying_contract);
136        let address_lower = lookup.verifying_contract.to_lowercase();
137        let primary_type = lookup.primary_type.clone();
138        let expected_hash = lookup.encode_type_hash.clone();
139        let result = self
140            .typed
141            .get(&key)
142            .map(|descriptors| {
143                descriptors
144                    .iter()
145                    .filter(|descriptor| {
146                        descriptor.display.formats.keys().any(|key| {
147                            let primary_matches = key == &primary_type
148                                || key.strip_prefix(&format!("{primary_type}(")).is_some();
149                            if !primary_matches {
150                                return false;
151                            }
152
153                            match expected_hash.as_deref() {
154                                Some(expected) if key.contains('(') => {
155                                    crate::eip712::format_key_hash_hex(key)
156                                        .eq_ignore_ascii_case(expected)
157                                }
158                                _ => true,
159                            }
160                        })
161                    })
162                    .cloned()
163                    .map(|descriptor| ResolvedDescriptor {
164                        descriptor,
165                        chain_id: lookup.chain_id,
166                        address: address_lower.clone(),
167                    })
168                    .collect::<Vec<_>>()
169            })
170            .unwrap_or_default();
171        let result = if result.is_empty() {
172            Err(ResolveError::NotFound {
173                chain_id: lookup.chain_id,
174                address: lookup.verifying_contract.clone(),
175            })
176        } else {
177            Ok(result)
178        };
179        Box::pin(async move { result })
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[tokio::test]
188    async fn test_static_source_not_found() {
189        let source = StaticSource::new();
190        let result = source.resolve_calldata(1, "0xabc").await;
191        assert!(result.is_err());
192    }
193}