1use alloy_primitives::{B256, keccak256};
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use foundry_cli::{opts::BuildOpts, utils::LoadConfig};
5use foundry_common::{compile::ProjectCompiler, shell};
6use serde::Serialize;
7use solar::sema::{
8 Gcx, Hir,
9 hir::StructId,
10 ty::{Ty, TyKind},
11};
12use std::{
13 collections::BTreeMap,
14 fmt::{Display, Formatter, Result as FmtResult, Write},
15 ops::ControlFlow,
16 path::{Path, PathBuf},
17};
18
19foundry_config::impl_figment_convert!(Eip712Args, build);
20
21#[derive(Clone, Debug, Parser)]
23pub struct Eip712Args {
24 #[arg(value_hint = ValueHint::FilePath, value_name = "PATH")]
26 pub target_path: PathBuf,
27
28 #[command(flatten)]
29 build: BuildOpts,
30}
31
32#[derive(Debug, Serialize)]
33struct Eip712Output {
34 path: String,
35 #[serde(rename = "type")]
36 ty: String,
37 hash: B256,
38}
39
40impl Display for Eip712Output {
41 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
42 writeln!(f, "{}:", self.path)?;
43 writeln!(f, " - type: {}", self.ty)?;
44 writeln!(f, " - hash: {}", self.hash)
45 }
46}
47
48impl Eip712Args {
49 pub fn run(self) -> Result<()> {
50 let config = self.build.load_config_with_dependencies()?;
51 let project = config.solar_project()?;
52 let mut output = ProjectCompiler::new().files([self.target_path]).compile(&project)?;
53 let compiler = output.parser_mut().solc_mut().compiler_mut();
54 compiler.dcx().set_allowed_diagnostic_codes(["5333".into()]);
56 compiler.enter_mut(|compiler| -> Result<()> {
57 let Ok(ControlFlow::Continue(())) = compiler.lower_asts() else { return Ok(()) };
58 let gcx = compiler.gcx();
59 let resolver = Resolver::new(gcx);
60
61 let outputs = resolver
62 .struct_ids()
63 .filter_map(|id| {
64 let resolved = resolver.resolve_struct_eip712(id)?;
65 Some(Eip712Output {
66 path: resolver.get_struct_path(id),
67 hash: keccak256(resolved.as_bytes()),
68 ty: resolved,
69 })
70 })
71 .collect::<Vec<_>>();
72
73 if shell::is_json() {
74 sh_println!("{json}", json = serde_json::to_string_pretty(&outputs)?)?;
75 } else {
76 for output in &outputs {
77 sh_eprintln!("{output}")?;
78 }
79 }
80
81 Ok(())
82 })?;
83
84 let diags = compiler.sess().dcx.emitted_diagnostics().unwrap();
86 if compiler.sess().dcx.has_errors().is_err() {
87 eyre::bail!("{diags}");
88 }
89 let _ = sh_eprint!("{diags}");
90
91 Ok(())
92 }
93}
94
95pub struct Resolver<'gcx> {
99 gcx: Gcx<'gcx>,
100}
101
102impl<'gcx> Resolver<'gcx> {
103 pub const fn new(gcx: Gcx<'gcx>) -> Self {
105 Self { gcx }
106 }
107
108 #[inline]
109 fn hir(&self) -> &'gcx Hir<'gcx> {
110 &self.gcx.hir
111 }
112
113 pub fn struct_ids(&self) -> impl Iterator<Item = StructId> {
115 self.hir().strukt_ids()
116 }
117
118 pub fn get_struct_path(&self, id: StructId) -> String {
120 let strukt = self.hir().strukt(id).name.as_str();
121 match self.hir().strukt(id).contract {
122 Some(cid) => {
123 let full_name = self.gcx.contract_fully_qualified_name(cid).to_string();
124 let relevant = Path::new(&full_name)
125 .file_name()
126 .and_then(|s| s.to_str())
127 .unwrap_or(&full_name);
128
129 if let Some((file, contract)) = relevant.rsplit_once(':') {
130 format!("{file} > {contract} > {strukt}")
131 } else {
132 format!("{relevant} > {strukt}")
133 }
134 }
135 None => strukt.to_string(),
136 }
137 }
138
139 pub fn resolve_struct_eip712(&self, id: StructId) -> Option<String> {
144 let mut subtypes = BTreeMap::new();
145 subtypes.insert(self.hir().strukt(id).name.as_str().into(), id);
146 self.resolve_eip712_inner(id, &mut subtypes, true, None)
147 }
148
149 fn resolve_eip712_inner(
150 &self,
151 id: StructId,
152 subtypes: &mut BTreeMap<String, StructId>,
153 append_subtypes: bool,
154 rename: Option<&str>,
155 ) -> Option<String> {
156 let def = self.hir().strukt(id);
157 let mut result = format!("{}(", rename.unwrap_or(def.name.as_str()));
158
159 for (idx, field_id) in def.fields.iter().enumerate() {
160 let field = self.hir().variable(*field_id);
161 let ty = self.resolve_type(self.gcx.type_of_hir_ty(&field.ty), subtypes)?;
162
163 write!(result, "{ty} {name}", name = field.name?.as_str()).ok()?;
164
165 if idx < def.fields.len() - 1 {
166 result.push(',');
167 }
168 }
169
170 result.push(')');
171
172 if append_subtypes {
173 for (subtype_name, subtype_id) in
174 subtypes.iter().map(|(name, id)| (name.clone(), *id)).collect::<Vec<_>>()
175 {
176 if subtype_id == id {
177 continue;
178 }
179 let encoded_subtype =
180 self.resolve_eip712_inner(subtype_id, subtypes, false, Some(&subtype_name))?;
181
182 result.push_str(&encoded_subtype);
183 }
184 }
185
186 Some(result)
187 }
188
189 fn resolve_type(
190 &self,
191 ty: Ty<'gcx>,
192 subtypes: &mut BTreeMap<String, StructId>,
193 ) -> Option<String> {
194 let ty = ty.peel_refs();
195 match ty.kind {
196 TyKind::Elementary(elem_ty) => Some(elem_ty.to_abi_str().to_string()),
197 TyKind::Array(element_ty, size) => {
198 let inner_type = self.resolve_type(element_ty, subtypes)?;
199 let size = size.to_string();
200 Some(format!("{inner_type}[{size}]"))
201 }
202 TyKind::DynArray(element_ty) => {
203 let inner_type = self.resolve_type(element_ty, subtypes)?;
204 Some(format!("{inner_type}[]"))
205 }
206 TyKind::Udvt(ty, _) => self.resolve_type(ty, subtypes),
207 TyKind::Struct(id) => {
208 let def = self.hir().strukt(id);
209 let name = match subtypes.iter().find(|(_, cached_id)| id == **cached_id) {
210 Some((name, _)) => name.clone(),
211 None => {
212 let mut i = 0;
214 let mut name = def.name.as_str().into();
215 while subtypes.contains_key(&name) {
216 i += 1;
217 name = format!("{}_{i}", def.name.as_str());
218 }
219
220 subtypes.insert(name.clone(), id);
221
222 for &field_id in def.fields {
224 let field_ty = self.gcx.type_of_item(field_id.into());
225 self.resolve_type(field_ty, subtypes)?;
226 }
227 name
228 }
229 };
230
231 Some(name)
232 }
233 TyKind::Enum(_) => Some("uint8".to_string()),
235 TyKind::Contract(_) => Some("address".to_string()),
237 _ => None,
239 }
240 }
241}