Skip to content

Commit af4e012

Browse files
committed
feat: "vm.parseJsonType"
1 parent 4b0df10 commit af4e012

File tree

4 files changed

+244
-3
lines changed

4 files changed

+244
-3
lines changed

crates/forge/bin/cmd/eip712.rs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
use clap::{Parser, ValueHint};
2+
use eyre::{Ok, OptionExt, Result};
3+
use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig};
4+
use foundry_common::compile::ProjectCompiler;
5+
use foundry_compilers::{
6+
artifacts::{
7+
output_selection::OutputSelection,
8+
visitor::{Visitor, Walk},
9+
ContractDefinition, EnumDefinition, SourceUnit, StructDefinition, TypeDescriptions,
10+
TypeName,
11+
},
12+
CompilerSettings,
13+
};
14+
use std::{collections::BTreeMap, path::PathBuf};
15+
16+
foundry_config::impl_figment_convert!(Eip712Args, opts);
17+
18+
/// CLI arguments for `forge eip712`.
19+
#[derive(Clone, Debug, Parser)]
20+
pub struct Eip712Args {
21+
/// The path to the file from which to read struct definitions.
22+
#[arg(value_hint = ValueHint::FilePath, value_name = "PATH")]
23+
pub target_path: PathBuf,
24+
25+
#[command(flatten)]
26+
opts: CoreBuildArgs,
27+
}
28+
29+
impl Eip712Args {
30+
pub fn run(self) -> Result<()> {
31+
let config = self.try_load_config_emit_warnings()?;
32+
let mut project = config.create_project(false, true)?;
33+
let target_path = dunce::canonicalize(self.target_path)?;
34+
project.settings.update_output_selection(|selection| {
35+
*selection = OutputSelection::ast_output_selection();
36+
});
37+
38+
let output = ProjectCompiler::new().files([target_path.clone()]).compile(&project)?;
39+
40+
// Collect ASTs by getting them from sources and converting into strongly typed
41+
// `SourceUnit`s.
42+
let asts = output
43+
.into_output()
44+
.sources
45+
.into_iter()
46+
.filter_map(|(path, mut sources)| Some((path, sources.swap_remove(0).source_file.ast?)))
47+
.map(|(path, ast)| {
48+
Ok((path, serde_json::from_str::<SourceUnit>(&serde_json::to_string(&ast)?)?))
49+
})
50+
.collect::<Result<BTreeMap<_, _>>>()?;
51+
52+
let resolver = Resolver::new(&asts);
53+
54+
let target_ast = asts
55+
.get(&target_path)
56+
.ok_or_else(|| eyre::eyre!("Could not find AST for target file {target_path:?}"))?;
57+
58+
let structs_in_target = {
59+
let mut collector = StructCollector::default();
60+
target_ast.walk(&mut collector);
61+
collector.0
62+
};
63+
64+
for (id, _) in structs_in_target {
65+
if let Some(resolved) =
66+
resolver.resolve_struct_eip712(id, &mut Default::default(), true)?
67+
{
68+
println!("{resolved}");
69+
println!();
70+
}
71+
}
72+
73+
Ok(())
74+
}
75+
}
76+
77+
#[derive(Debug, Clone, Default)]
78+
struct StructCollector(BTreeMap<usize, StructDefinition>);
79+
80+
impl Visitor for StructCollector {
81+
fn visit_struct_definition(&mut self, def: &StructDefinition) {
82+
self.0.insert(def.id, def.clone());
83+
}
84+
}
85+
86+
/// Collects mapping from AST id of type definition to representation of this type for EIP-712
87+
/// encoding.
88+
///
89+
/// For now, maps contract definitions to address and enums to uint8
90+
#[derive(Debug, Clone, Default)]
91+
struct SimpleCustomTypesCollector(BTreeMap<usize, String>);
92+
93+
impl Visitor for SimpleCustomTypesCollector {
94+
fn visit_contract_definition(&mut self, def: &ContractDefinition) {
95+
self.0.insert(def.id, "address".to_string());
96+
}
97+
98+
fn visit_enum_definition(&mut self, def: &EnumDefinition) {
99+
self.0.insert(def.id, "uint8".to_string());
100+
}
101+
}
102+
103+
pub struct Resolver {
104+
simple_types: BTreeMap<usize, String>,
105+
structs: BTreeMap<usize, StructDefinition>,
106+
}
107+
108+
impl Resolver {
109+
pub fn new(asts: &BTreeMap<PathBuf, SourceUnit>) -> Self {
110+
let simple_types = {
111+
let mut collector = SimpleCustomTypesCollector::default();
112+
asts.values().for_each(|ast| ast.walk(&mut collector));
113+
114+
collector.0
115+
};
116+
117+
let structs = {
118+
let mut collector = StructCollector::default();
119+
asts.values().for_each(|ast| ast.walk(&mut collector));
120+
collector.0
121+
};
122+
123+
Self { simple_types, structs }
124+
}
125+
126+
pub fn resolve_struct_eip712(
127+
&self,
128+
id: usize,
129+
subtypes: &mut BTreeMap<String, usize>,
130+
append_subtypes: bool,
131+
) -> Result<Option<String>> {
132+
let def = &self.structs[&id];
133+
let mut result = format!("{}(", def.name);
134+
135+
for (idx, member) in def.members.iter().enumerate() {
136+
let Some(ty) = self.resolve_type(
137+
member.type_name.as_ref().ok_or_eyre("missing type name")?,
138+
subtypes,
139+
)?
140+
else {
141+
return Ok(None)
142+
};
143+
144+
result.push_str(&ty);
145+
result.push(' ');
146+
result.push_str(&member.name);
147+
148+
if idx < def.members.len() - 1 {
149+
result.push(',');
150+
}
151+
}
152+
153+
result.push(')');
154+
155+
if !append_subtypes {
156+
return Ok(Some(result))
157+
}
158+
159+
for subtype_id in subtypes.values().copied().collect::<Vec<_>>() {
160+
if subtype_id == id {
161+
continue
162+
}
163+
let Some(encoded_subtype) = self.resolve_struct_eip712(subtype_id, subtypes, false)?
164+
else {
165+
return Ok(None)
166+
};
167+
result.push_str(&encoded_subtype);
168+
}
169+
170+
Ok(Some(result))
171+
}
172+
173+
/// Converts given [TypeName] into a type which can be converted to [DynSolType].
174+
///
175+
/// Returns `None` if the type is not supported for EIP712 encoding.
176+
pub fn resolve_type(
177+
&self,
178+
type_name: &TypeName,
179+
subtypes: &mut BTreeMap<String, usize>,
180+
) -> Result<Option<String>> {
181+
match type_name {
182+
TypeName::FunctionTypeName(_) | TypeName::Mapping(_) => Ok(None),
183+
TypeName::ElementaryTypeName(ty) => Ok(Some(ty.name.clone())),
184+
TypeName::ArrayTypeName(ty) => {
185+
let Some(inner) = self.resolve_type(&ty.base_type, subtypes)? else {
186+
return Ok(None)
187+
};
188+
let len = parse_array_length(&ty.type_descriptions)?;
189+
190+
Ok(Some(format!("{inner}[{}]", len.unwrap_or(""))))
191+
}
192+
TypeName::UserDefinedTypeName(ty) => {
193+
if let Some(name) = self.simple_types.get(&(ty.referenced_declaration as usize)) {
194+
return Ok(Some(name.clone()))
195+
} else if let Some(def) = self.structs.get(&(ty.referenced_declaration as usize)) {
196+
let name =
197+
// If we've already seen struct with this ID, just use assigned name.
198+
if let Some((name, _)) = subtypes.iter().find(|(_, id)| **id == def.id) {
199+
name.clone()
200+
// Otherwise, try assigning a new name.
201+
} else {
202+
let mut i = 0;
203+
let mut name = def.name.clone();
204+
while subtypes.contains_key(&name) {
205+
i += 1;
206+
name = format!("{}_{i}", def.name);
207+
}
208+
209+
subtypes.insert(name.clone(), def.id);
210+
name
211+
};
212+
213+
return Ok(Some(name))
214+
} else {
215+
return Ok(None)
216+
}
217+
}
218+
}
219+
}
220+
}
221+
222+
fn parse_array_length(type_description: &TypeDescriptions) -> Result<Option<&str>> {
223+
let type_string =
224+
type_description.type_string.as_ref().ok_or_eyre("missing typeString for array type")?;
225+
let Some(inside_brackets) =
226+
type_string.rsplit_once("[").and_then(|(_, right)| right.split("]").next())
227+
else {
228+
eyre::bail!("failed to parse array type string: {type_string}")
229+
};
230+
231+
if inside_brackets.is_empty() {
232+
return Ok(None)
233+
} else {
234+
return Ok(Some(inside_brackets))
235+
}
236+
}

crates/forge/bin/cmd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub mod coverage;
4848
pub mod create;
4949
pub mod debug;
5050
pub mod doc;
51+
pub mod eip712;
5152
pub mod flatten;
5253
pub mod fmt;
5354
pub mod geiger;

crates/forge/bin/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ fn main() -> Result<()> {
112112
},
113113
ForgeSubcommand::VerifyBytecode(cmd) => utils::block_on(cmd.run()),
114114
ForgeSubcommand::Soldeer(cmd) => cmd.run(),
115+
ForgeSubcommand::Eip712(cmd) => cmd.run(),
115116
}
116117
}
117118

crates/forge/bin/opts.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::cmd::{
22
bind::BindArgs, build::BuildArgs, cache::CacheArgs, clone::CloneArgs, config, coverage,
3-
create::CreateArgs, debug::DebugArgs, doc::DocArgs, flatten, fmt::FmtArgs, geiger, generate,
4-
init::InitArgs, inspect, install::InstallArgs, remappings::RemappingArgs, remove::RemoveArgs,
5-
selectors::SelectorsSubcommands, snapshot, soldeer, test, tree, update,
3+
create::CreateArgs, debug::DebugArgs, doc::DocArgs, eip712, flatten, fmt::FmtArgs, geiger,
4+
generate, init::InitArgs, inspect, install::InstallArgs, remappings::RemappingArgs,
5+
remove::RemoveArgs, selectors::SelectorsSubcommands, snapshot, soldeer, test, tree, update,
66
};
77
use clap::{Parser, Subcommand, ValueHint};
88
use forge_script::ScriptArgs;
@@ -164,6 +164,9 @@ pub enum ForgeSubcommand {
164164

165165
/// Soldeer dependency manager.
166166
Soldeer(soldeer::SoldeerArgs),
167+
168+
/// Generate EIP-712 struct encodings for structs from a given file.
169+
Eip712(eip712::Eip712Args),
167170
}
168171

169172
#[cfg(test)]

0 commit comments

Comments
 (0)