1use alloy_primitives::{B256, keccak256};
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use foundry_cli::opts::{BuildOpts, solar_pcx_from_build_opts};
5use serde::Serialize;
6use solar_parse::interface::Session;
7use solar_sema::{
8 GcxWrapper, Hir,
9 hir::StructId,
10 thread_local::ThreadLocal,
11 ty::{Ty, TyKind},
12};
13use std::{
14 collections::BTreeMap,
15 fmt::{Display, Formatter, Result as FmtResult, Write},
16 path::{Path, PathBuf},
17 slice,
18};
19
20foundry_config::impl_figment_convert!(Eip712Args, build);
21
22#[derive(Clone, Debug, Parser)]
24pub struct Eip712Args {
25 #[arg(value_hint = ValueHint::FilePath, value_name = "PATH")]
27 pub target_path: PathBuf,
28
29 #[arg(long, help = "Output in JSON format")]
31 pub json: bool,
32
33 #[command(flatten)]
34 build: BuildOpts,
35}
36
37#[derive(Debug, Serialize)]
38struct Eip712Output {
39 path: String,
40 #[serde(rename = "type")]
41 ty: String,
42 hash: B256,
43}
44
45impl Display for Eip712Output {
46 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
47 writeln!(f, "{}:", self.path)?;
48 writeln!(f, " - type: {}", self.ty)?;
49 writeln!(f, " - hash: {}", self.hash)
50 }
51}
52
53impl Eip712Args {
54 pub fn run(self) -> Result<()> {
55 let mut sess = Session::builder().with_stderr_emitter().build();
56 sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false);
57
58 sess.enter_parallel(|| -> Result<()> {
59 let parsing_context = solar_pcx_from_build_opts(
61 &sess,
62 &self.build,
63 None,
64 Some(slice::from_ref(&self.target_path)),
65 )?;
66
67 let hir_arena = ThreadLocal::new();
69 let Ok(Some(gcx)) = parsing_context.parse_and_lower(&hir_arena) else {
70 return Err(eyre::eyre!("failed parsing"));
71 };
72 let resolver = Resolver::new(gcx);
73
74 let outputs = resolver
75 .struct_ids()
76 .filter_map(|id| {
77 let resolved = resolver.resolve_struct_eip712(id)?;
78 Some(Eip712Output {
79 path: resolver.get_struct_path(id),
80 hash: keccak256(resolved.as_bytes()),
81 ty: resolved,
82 })
83 })
84 .collect::<Vec<_>>();
85
86 if self.json {
87 sh_println!("{json}", json = serde_json::to_string_pretty(&outputs)?)?;
88 } else {
89 for output in &outputs {
90 sh_println!("{output}")?;
91 }
92 }
93
94 Ok(())
95 })?;
96
97 eyre::ensure!(sess.dcx.has_errors().is_ok(), "errors occurred");
98
99 Ok(())
100 }
101}
102
103pub struct Resolver<'hir> {
107 gcx: GcxWrapper<'hir>,
108}
109
110impl<'hir> Resolver<'hir> {
111 pub fn new(gcx: GcxWrapper<'hir>) -> Self {
113 Self { gcx }
114 }
115
116 #[inline]
117 fn hir(&self) -> &'hir Hir<'hir> {
118 &self.gcx.get().hir
119 }
120
121 pub fn struct_ids(&self) -> impl Iterator<Item = StructId> {
123 self.hir().strukt_ids()
124 }
125
126 pub fn get_struct_path(&self, id: StructId) -> String {
128 let strukt = self.hir().strukt(id).name.as_str();
129 match self.hir().strukt(id).contract {
130 Some(cid) => {
131 let full_name = self.gcx.get().contract_fully_qualified_name(cid).to_string();
132 let relevant = Path::new(&full_name)
133 .file_name()
134 .and_then(|s| s.to_str())
135 .unwrap_or(&full_name);
136
137 if let Some((file, contract)) = relevant.rsplit_once(':') {
138 format!("{file} > {contract} > {strukt}")
139 } else {
140 format!("{relevant} > {strukt}")
141 }
142 }
143 None => strukt.to_string(),
144 }
145 }
146
147 pub fn resolve_struct_eip712(&self, id: StructId) -> Option<String> {
152 let mut subtypes = BTreeMap::new();
153 subtypes.insert(self.hir().strukt(id).name.as_str().into(), id);
154 self.resolve_eip712_inner(id, &mut subtypes, true, None)
155 }
156
157 fn resolve_eip712_inner(
158 &self,
159 id: StructId,
160 subtypes: &mut BTreeMap<String, StructId>,
161 append_subtypes: bool,
162 rename: Option<&str>,
163 ) -> Option<String> {
164 let def = self.hir().strukt(id);
165 let mut result = format!("{}(", rename.unwrap_or(def.name.as_str()));
166
167 for (idx, field_id) in def.fields.iter().enumerate() {
168 let field = self.hir().variable(*field_id);
169 let ty = self.resolve_type(self.gcx.get().type_of_hir_ty(&field.ty), subtypes)?;
170
171 write!(result, "{ty} {name}", name = field.name?.as_str()).ok()?;
172
173 if idx < def.fields.len() - 1 {
174 result.push(',');
175 }
176 }
177
178 result.push(')');
179
180 if append_subtypes {
181 for (subtype_name, subtype_id) in
182 subtypes.iter().map(|(name, id)| (name.clone(), *id)).collect::<Vec<_>>()
183 {
184 if subtype_id == id {
185 continue;
186 }
187 let encoded_subtype =
188 self.resolve_eip712_inner(subtype_id, subtypes, false, Some(&subtype_name))?;
189
190 result.push_str(&encoded_subtype);
191 }
192 }
193
194 Some(result)
195 }
196
197 fn resolve_type(
198 &self,
199 ty: Ty<'hir>,
200 subtypes: &mut BTreeMap<String, StructId>,
201 ) -> Option<String> {
202 let ty = ty.peel_refs();
203 match ty.kind {
204 TyKind::Elementary(elem_ty) => Some(elem_ty.to_abi_str().to_string()),
205 TyKind::Array(element_ty, size) => {
206 let inner_type = self.resolve_type(element_ty, subtypes)?;
207 let size = size.to_string();
208 Some(format!("{inner_type}[{size}]"))
209 }
210 TyKind::DynArray(element_ty) => {
211 let inner_type = self.resolve_type(element_ty, subtypes)?;
212 Some(format!("{inner_type}[]"))
213 }
214 TyKind::Udvt(ty, _) => self.resolve_type(ty, subtypes),
215 TyKind::Struct(id) => {
216 let def = self.hir().strukt(id);
217 let name = match subtypes.iter().find(|(_, cached_id)| id == **cached_id) {
218 Some((name, _)) => name.to_string(),
219 None => {
220 let mut i = 0;
222 let mut name = def.name.as_str().into();
223 while subtypes.contains_key(&name) {
224 i += 1;
225 name = format!("{}_{i}", def.name.as_str());
226 }
227
228 subtypes.insert(name.clone(), id);
229
230 for &field_id in def.fields {
232 let field_ty = self.gcx.get().type_of_item(field_id.into());
233 self.resolve_type(field_ty, subtypes)?;
234 }
235 name
236 }
237 };
238
239 Some(name)
240 }
241 TyKind::Enum(_) => Some("uint8".to_string()),
243 TyKind::Contract(_) => Some("address".to_string()),
245 _ => None,
247 }
248 }
249}