Skip to content

Commit 7ca3203

Browse files
committed
Generate named struct types for Solidity tuples with named components
When a Solidity struct is ABI-encoded, it becomes a tuple. Previously, the codegen always generated unnamed TypeScript tuple types (arrays) for these, making it hard for developers to access fields by name. Now, when a tuple has all named components (indicating a Solidity struct), the codegen generates a named record type with proper field names. This applies to the ReScript types (with @genType for TypeScript generation), the rescript-schema declarations, and the HyperSync decoder conversion. Key changes: - abi_compat.rs: Preserve component names from alloy's EventParam - event_parsing.rs: Add AuxTypeDecl and abi_to_rescript_type_with_structs that generates named record types for structs, with schemas that read from arrays but produce records, and fromArray converters for HyperSync - codegen_templates.rs: Use struct-aware type conversion, generate aux type declarations in event modules, update HyperSync converter to construct records from decoded arrays for struct params Unnamed tuples (without named components) remain as before. https://claude.ai/code/session_01VDr9kuWFkdSN5dsCoD9K5P
1 parent ff3e6e1 commit 7ca3203

File tree

4 files changed

+535
-32
lines changed

4 files changed

+535
-32
lines changed

codegenerator/cli/src/config_parsing/abi_compat.rs

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
//! ethers-based implementation.
66
77
use alloy_dyn_abi::DynSolType;
8-
use alloy_json_abi::{Event as AlloyEvent, EventParam as AlloyEventParam};
8+
use alloy_json_abi::{
9+
Event as AlloyEvent, EventParam as AlloyEventParam, Param as AlloyParam,
10+
};
911
use anyhow::{anyhow, Context, Result};
1012
use std::str::FromStr;
1113

1214
/// A wrapper around an event parameter that provides a similar API to the old ethers EventParam.
1315
///
1416
/// This struct stores the parsed `DynSolType` directly, unlike alloy's `EventParam`
15-
/// which stores the type as a string.
17+
/// which stores the type as a string. It also preserves component names from struct
18+
/// types, enabling generation of named object types instead of unnamed tuples.
1619
#[derive(Debug, Clone, PartialEq)]
1720
pub struct EventParam {
1821
/// The parameter's name
@@ -21,12 +24,17 @@ pub struct EventParam {
2124
pub kind: DynSolType,
2225
/// Whether the parameter is indexed
2326
pub indexed: bool,
27+
/// Named components for struct (tuple) types.
28+
/// When a Solidity struct is ABI-encoded, it becomes a tuple with named components.
29+
/// Preserving these names allows generating object types with named fields
30+
/// instead of positional tuple types.
31+
pub components: Vec<EventParam>,
2432
}
2533

2634
impl EventParam {
2735
/// Create a new EventParam from alloy's EventParam.
2836
///
29-
/// This parses the type string into a DynSolType.
37+
/// This parses the type string into a DynSolType and preserves component names.
3038
pub fn try_from_alloy(param: &AlloyEventParam) -> Result<Self> {
3139
let type_str = param.selector_type();
3240
let kind = DynSolType::parse(&type_str).with_context(|| {
@@ -35,10 +43,38 @@ impl EventParam {
3543
type_str, param.name
3644
)
3745
})?;
46+
let components = param
47+
.components
48+
.iter()
49+
.map(Self::try_from_alloy_param)
50+
.collect::<Result<Vec<_>>>()?;
3851
Ok(EventParam {
3952
name: param.name.clone(),
4053
kind,
4154
indexed: param.indexed,
55+
components,
56+
})
57+
}
58+
59+
/// Create a new EventParam from alloy's Param (used for nested components).
60+
fn try_from_alloy_param(param: &AlloyParam) -> Result<Self> {
61+
let type_str = param.selector_type();
62+
let kind = DynSolType::parse(&type_str).with_context(|| {
63+
format!(
64+
"Failed to parse type '{}' for parameter '{}'",
65+
type_str, param.name
66+
)
67+
})?;
68+
let components = param
69+
.components
70+
.iter()
71+
.map(Self::try_from_alloy_param)
72+
.collect::<Result<Vec<_>>>()?;
73+
Ok(EventParam {
74+
name: param.name.clone(),
75+
kind,
76+
indexed: false,
77+
components,
4278
})
4379
}
4480

@@ -48,6 +84,7 @@ impl EventParam {
4884
name,
4985
kind,
5086
indexed,
87+
components: vec![],
5188
}
5289
}
5390
}
@@ -125,6 +162,47 @@ mod tests {
125162
assert_eq!(event.name, "MyEvent");
126163
assert_eq!(event.inputs.len(), 1);
127164
assert!(matches!(event.inputs[0].kind, DynSolType::Tuple(_)));
165+
// Unnamed tuple components should have empty names
166+
assert_eq!(event.inputs[0].components.len(), 2);
167+
assert_eq!(event.inputs[0].components[0].name, "");
168+
assert_eq!(event.inputs[0].components[1].name, "");
169+
}
170+
171+
#[test]
172+
fn test_parse_event_with_named_tuple_components() {
173+
// Simulates a struct with named fields parsed from JSON ABI
174+
use alloy_json_abi::{Event as AlloyEvent, EventParam as AlloyEventParam, Param};
175+
176+
let alloy_event = AlloyEvent {
177+
name: "MyEvent".to_string(),
178+
inputs: vec![AlloyEventParam {
179+
ty: "tuple".to_string(),
180+
name: "data".to_string(),
181+
indexed: false,
182+
components: vec![
183+
Param {
184+
ty: "address".to_string(),
185+
name: "funder".to_string(),
186+
components: vec![],
187+
internal_type: None,
188+
},
189+
Param {
190+
ty: "uint256".to_string(),
191+
name: "amount".to_string(),
192+
components: vec![],
193+
internal_type: None,
194+
},
195+
],
196+
internal_type: None,
197+
}],
198+
anonymous: false,
199+
};
200+
201+
let event = Event::try_from_alloy(&alloy_event).unwrap();
202+
assert_eq!(event.inputs.len(), 1);
203+
assert_eq!(event.inputs[0].components.len(), 2);
204+
assert_eq!(event.inputs[0].components[0].name, "funder");
205+
assert_eq!(event.inputs[0].components[1].name, "amount");
128206
}
129207

130208
#[test]

0 commit comments

Comments
 (0)