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