Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 184 additions & 29 deletions gnd/src/scaffold/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

use std::collections::HashMap;

use serde_json::Value;

use super::ScaffoldOptions;
use crate::shared::handle_reserved_word;

Expand Down Expand Up @@ -178,6 +180,8 @@ pub struct EventInput {
pub name: String,
pub solidity_type: String,
pub indexed: bool,
/// Nested fields for `tuple` / `tuple[]` inputs; empty otherwise.
pub components: Vec<EventInput>,
}

/// Extract events from ABI JSON.
Expand All @@ -204,23 +208,7 @@ pub fn extract_events_from_abi(options: &ScaffoldOptions) -> Vec<EventInfo> {
let inputs = item
.get("inputs")
.and_then(|i| i.as_array())
.map(|arr| {
arr.iter()
.filter_map(|input| {
let name = input.get("name").and_then(|n| n.as_str())?.to_string();
let solidity_type = input.get("type").and_then(|t| t.as_str())?.to_string();
let indexed = input
.get("indexed")
.and_then(|i| i.as_bool())
.unwrap_or(false);
Some(EventInput {
name,
solidity_type,
indexed,
})
})
.collect::<Vec<_>>()
})
.map(|arr| arr.iter().filter_map(parse_event_input).collect::<Vec<_>>())
.unwrap_or_default();

let signature = format_event_signature(name, &inputs);
Expand All @@ -235,22 +223,123 @@ pub fn extract_events_from_abi(options: &ScaffoldOptions) -> Vec<EventInfo> {
events
}

/// Format an event signature string.
fn format_event_signature(name: &str, inputs: &[EventInput]) -> String {
let params: Vec<String> = inputs
.iter()
.map(|input| {
if input.indexed {
format!("indexed {}", input.solidity_type)
} else {
input.solidity_type.clone()
}
})
.collect();
/// Recursively parse an ABI input, including nested tuple components.
fn parse_event_input(input: &Value) -> Option<EventInput> {
let name = input.get("name").and_then(|n| n.as_str())?.to_string();
let solidity_type = input.get("type").and_then(|t| t.as_str())?.to_string();
let indexed = input
.get("indexed")
.and_then(|i| i.as_bool())
.unwrap_or(false);
let components = input
.get("components")
.and_then(|c| c.as_array())
.map(|arr| arr.iter().filter_map(parse_event_input).collect())
.unwrap_or_default();
Some(EventInput {
name,
solidity_type,
indexed,
components,
})
}

/// Format an event signature, recursing into tuple components so the topic0
/// hash matches the on-chain event, e.g. `Foo((address,uint256),uint256)`.
fn format_event_signature(name: &str, inputs: &[EventInput]) -> String {
let params: Vec<String> = inputs.iter().map(signature_param).collect();
format!("{}({})", name, params.join(","))
}

/// One parameter's signature, preserving the `indexed` marker at the top level.
fn signature_param(input: &EventInput) -> String {
let ty = signature_type(input);
if input.indexed {
format!("indexed {}", ty)
} else {
ty
}
}

/// Canonical Solidity type for a signature; a tuple expands to its components.
fn signature_type(input: &EventInput) -> String {
match tuple_suffix(&input.solidity_type) {
Some(suffix) => {
let inner: Vec<String> = input.components.iter().map(signature_type).collect();
format!("({}){}", inner.join(","), suffix)
}
None => input.solidity_type.clone(),
}
}

/// If `t` is `tuple`, `tuple[]`, `tuple[N]`, … returns the array suffix (possibly
/// empty); otherwise `None`.
fn tuple_suffix(t: &str) -> Option<&str> {
let rest = t.strip_prefix("tuple")?;
if rest.is_empty() || rest.starts_with('[') {
Some(rest)
} else {
None
}
}

/// A flattened leaf of an event input: the entity field name, the matching
/// `event.params` accessor, and the leaf's Solidity type. A single `tuple`
/// expands to one leaf per component (`data` -> `data_a`, `data_b`).
pub struct InputLeaf {
pub field: String,
pub accessor: String,
pub solidity_type: String,
}

/// Flatten an event's inputs into leaves, unrolling a single `tuple` into its
/// components (`tuple[]` stays one leaf, handled as a Bytes array downstream).
/// The top-level accessor mirrors the generated binding getter
/// (`event_param_accessors`) so `event.params.<x>` resolves; field names are
/// sanitized for the schema/entity side.
pub fn flatten_event_inputs(inputs: &[EventInput]) -> Vec<InputLeaf> {
let accessors = event_param_accessors(inputs);
let mut leaves = Vec::new();
for (input, accessor) in inputs.iter().zip(&accessors) {
flatten_input_into(
&mut leaves,
std::slice::from_ref(accessor),
&[super::sanitize_field_name(&input.name)],
input,
);
}
leaves
}

fn flatten_input_into(
out: &mut Vec<InputLeaf>,
accessor_path: &[String],
field_path: &[String],
input: &EventInput,
) {
if input.solidity_type != "tuple" {
out.push(InputLeaf {
field: field_path.join("_"),
accessor: accessor_path.join("."),
solidity_type: input.solidity_type.clone(),
});
return;
}

for (i, comp) in input.components.iter().enumerate() {
let (raw, field) = if comp.name.is_empty() {
(format!("value{i}"), format!("value{i}"))
} else {
(comp.name.clone(), super::sanitize_field_name(&comp.name))
};
let mut accessor = accessor_path.to_vec();
accessor.push(raw);
let mut fields = field_path.to_vec();
fields.push(field);
flatten_input_into(out, &accessor, &fields, comp);
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -385,18 +474,84 @@ mod tests {
name: "from".to_string(),
solidity_type: "address".to_string(),
indexed: true,
components: vec![],
},
EventInput {
name: "value".to_string(),
solidity_type: "uint256".to_string(),
indexed: false,
components: vec![],
},
];

let sig = format_event_signature("Transfer", &inputs);
assert_eq!(sig, "Transfer(indexed address,uint256)");
}

#[test]
fn test_format_event_signature_tuple() {
let scalar = |name: &str, ty: &str| EventInput {
name: name.to_string(),
solidity_type: ty.to_string(),
indexed: false,
components: vec![],
};

// A tuple param expands to its components so topic0 matches the chain.
let inputs = vec![
EventInput {
name: "data".to_string(),
solidity_type: "tuple".to_string(),
indexed: false,
components: vec![scalar("account", "address"), scalar("amount", "uint256")],
},
scalar("id", "uint256"),
];
assert_eq!(
format_event_signature("Foo", &inputs),
"Foo((address,uint256),uint256)"
);

// Arrays of tuples keep the array suffix.
let arr = vec![EventInput {
name: "items".to_string(),
solidity_type: "tuple[]".to_string(),
indexed: false,
components: vec![scalar("a", "address")],
}];
assert_eq!(format_event_signature("Bar", &arr), "Bar((address)[])");
}

#[test]
fn test_flatten_input_tuple() {
let scalar = |name: &str, ty: &str| EventInput {
name: name.to_string(),
solidity_type: ty.to_string(),
indexed: false,
components: vec![],
};

// A tuple flattens into one leaf per component.
let tuple = EventInput {
name: "data".to_string(),
solidity_type: "tuple".to_string(),
indexed: false,
components: vec![scalar("account", "address"), scalar("amount", "uint256")],
};
let leaves = flatten_event_inputs(&[tuple]);
assert_eq!(leaves.len(), 2);
assert_eq!(leaves[0].field, "data_account");
assert_eq!(leaves[0].accessor, "data.account");
assert_eq!(leaves[1].field, "data_amount");
assert_eq!(leaves[1].accessor, "data.amount");

// A non-tuple input is a single leaf.
let leaves = flatten_event_inputs(&[scalar("from", "address")]);
assert_eq!(leaves.len(), 1);
assert_eq!(leaves[0].field, "from");
assert_eq!(leaves[0].accessor, "from");
}

#[test]
fn test_disambiguate_events() {
let ev = |name: &str| EventInfo {
Expand Down
79 changes: 70 additions & 9 deletions gnd/src/scaffold/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,22 @@ fn generate_single_handler(resolved: &ResolvedEvent) -> String {
let alias = &resolved.alias;
let entity_name = &resolved.entity_name;

// Generate field assignments from event parameters. The accessor mirrors the
// generated binding getter (escaped reserved words, param<index> for unnamed).
// Generate field assignments: tuples are unrolled, arrays of address/tuple
// are changetype'd, and accessors mirror the generated binding getters
// (escaped reserved words, param<index> for unnamed).
let mut field_assignments = String::new();
let accessors = super::event_param_accessors(&resolved.event.inputs);
for (input, accessor) in resolved.event.inputs.iter().zip(&accessors) {
let field_name = sanitize_field_name(&input.name);
field_assignments.push_str(&format!(
" entity.{} = event.params.{}\n",
field_name, accessor
));
for leaf in super::flatten_event_inputs(&resolved.event.inputs) {
if needs_bytes_array_cast(&leaf.solidity_type) {
field_assignments.push_str(&format!(
" entity.{} = changetype<Bytes[]>(event.params.{})\n",
leaf.field, leaf.accessor
));
} else {
field_assignments.push_str(&format!(
" entity.{} = event.params.{}\n",
leaf.field, leaf.accessor
));
}
}

format!(
Expand All @@ -229,6 +235,24 @@ fn generate_single_handler(resolved: &ResolvedEvent) -> String {
)
}

/// Whether a leaf type is a (possibly fixed-size) array of `address` or `tuple`.
/// The bindings expose these as `Array<Address>` / `Array<ethereum.Tuple>`, which
/// must be `changetype`'d to fit a `Bytes[]` entity field.
fn needs_bytes_array_cast(solidity_type: &str) -> bool {
["address", "tuple"]
.iter()
.any(|base| match solidity_type.strip_prefix(base) {
Some("[]") => true,
Some(rest) => rest
.strip_prefix('[')
.and_then(|r| r.strip_suffix(']'))
.is_some_and(|inner| {
!inner.is_empty() && inner.chars().all(|c| c.is_ascii_digit())
}),
None => false,
})
}

/// Extract callable functions from ABI for documentation comments.
fn extract_callable_functions(options: &ScaffoldOptions) -> String {
let Some(abi) = &options.abi else {
Expand Down Expand Up @@ -435,4 +459,41 @@ mod tests {
mapping
);
}

#[test]
fn test_generate_mapping_tuple_and_array() {
let abi = json!([
{
"type": "event",
"name": "Deposit",
"inputs": [
{"name": "data", "type": "tuple", "components": [
{"name": "account", "type": "address"}
]},
{"name": "owners", "type": "address[]"}
]
}
]);

let options = ScaffoldOptions {
contract_name: "Vault".to_string(),
abi: Some(abi),
index_events: true,
..Default::default()
};

let mapping = generate_mapping(&options);
// Tuple components are unrolled into nested accessors.
assert!(
mapping.contains("entity.data_account = event.params.data.account"),
"{}",
mapping
);
// Arrays of address are changetype'd to Bytes[].
assert!(
mapping.contains("entity.owners = changetype<Bytes[]>(event.params.owners)"),
"{}",
mapping
);
}
}
4 changes: 2 additions & 2 deletions gnd/src/scaffold/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ mod naming;
mod schema;

pub use manifest::{
EventInfo, EventInput, ResolvedEvent, disambiguate_events, event_param_accessors,
extract_events_from_abi, generate_manifest,
EventInfo, EventInput, InputLeaf, ResolvedEvent, disambiguate_events, event_param_accessors,
extract_events_from_abi, flatten_event_inputs, generate_manifest,
};
pub use mapping::{generate_event_handlers, generate_mapping};
pub(crate) use naming::sanitize_field_name;
Expand Down
Loading