From dd89ae58a54a4e54829f334e461bab9e25c0d8c0 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Tue, 21 Jul 2026 19:00:35 +0530 Subject: [PATCH 1/3] gnd: Carry resolved DynSolType through scaffold generators The scaffold generators carried Solidity types as raw strings and matched them with `ends_with("[]")` / `starts_with("uint")`, while codegen carries alloy `DynSolType` and matches on variants. Every scaffold bug found while reviewing tuple support was a string-matching bug in this half. Resolve each event input to `DynSolType` once at the ABI boundary and carry the typed value through `InputLeaf`, the schema generator and the mapping cast. `solidity_to_graphql`/`int_to_graphql` become `graphql_type`, an exhaustive match on the type; `needs_bytes_array_cast` matches `Array(Address)` instead of the `"address[]"` string. Typing the schema side also fixes fixed-size arrays of scalar elements (`uint256[3]`, `bool[2]`, `bytes32[4]`, ...), which `ends_with("[]")` could not see and which scaffolded to a non-building scalar field. `address[3]` still needs the cast and is handled in the next commit. Scaffolding runs on hand-written ABIs alloy accepts but cannot fully resolve (a component-less `tuple`), so a fallible resolver falls back to a `Bytes` field with a warning instead of panicking like codegen's resolver does. --- gnd/src/abi.rs | 10 +++ gnd/src/scaffold/manifest.rs | 34 ++++++-- gnd/src/scaffold/mapping.rs | 8 +- gnd/src/scaffold/schema.rs | 159 +++++++++++++++++------------------ 4 files changed, 118 insertions(+), 93 deletions(-) diff --git a/gnd/src/abi.rs b/gnd/src/abi.rs index 08948367dd6..5b9cd402a55 100644 --- a/gnd/src/abi.rs +++ b/gnd/src/abi.rs @@ -54,6 +54,16 @@ pub fn resolve_event_param_type(param: &EventParam) -> DynSolType { .expect("valid ABI type") } +/// Parse a selector type without panicking. +/// +/// [`resolve_event_param_type`] `.expect()`s because codegen only runs on ABIs +/// that already resolved. Scaffolding also runs on hand-written ABIs that alloy +/// accepts but cannot fully resolve (a component-less `tuple`), so it must fall +/// back to a `Bytes` field rather than abort. Returns `None` for such a type. +pub fn try_resolve_selector_type(selector_type: &str) -> Option { + selector_type.parse::().ok() +} + /// The type an indexed param actually carries in the log. /// /// A topic is exactly 32 bytes, so reference types (strings, bytes, arrays and diff --git a/gnd/src/scaffold/manifest.rs b/gnd/src/scaffold/manifest.rs index 31684e0d502..83d08b56ca0 100644 --- a/gnd/src/scaffold/manifest.rs +++ b/gnd/src/scaffold/manifest.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; -use graph::abi::{Event, EventParam, Param}; +use graph::abi::{DynSolType, Event, EventParam, Param}; use super::ScaffoldOptions; use crate::shared::handle_reserved_word; @@ -179,12 +179,12 @@ pub fn event_param_accessors(inputs: &[EventParam]) -> Vec { } /// 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` +/// `event.params` accessor, and the leaf's resolved 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, + pub ty: DynSolType, } /// Flatten an event's inputs into leaves, unrolling a single `tuple` into its @@ -203,7 +203,7 @@ pub fn flatten_event_inputs(inputs: &[EventParam]) -> Vec { leaves.push(InputLeaf { field: super::sanitize_field_name(&input.name), accessor: accessor.clone(), - solidity_type: "bytes32".to_string(), + ty: DynSolType::FixedBytes(32), }); continue; } @@ -211,7 +211,7 @@ pub fn flatten_event_inputs(inputs: &[EventParam]) -> Vec { &mut leaves, std::slice::from_ref(accessor), &[super::sanitize_field_name(&input.name)], - &input.ty, + &input.selector_type(), &input.components, ); } @@ -238,14 +238,24 @@ fn flatten_into( out: &mut Vec, accessor_path: &[String], field_path: &[String], - solidity_type: &str, + selector_type: &str, components: &[Param], ) { - if solidity_type != "tuple" { + // `Tuple(_)` is the unroll discriminator; its payload is never read (names and + // recursion come from `components`), so dropping the component names alloy does + // is harmless. `tuple[]` resolves to `Array(Tuple(_))` and stays a leaf. + let resolved = crate::abi::try_resolve_selector_type(selector_type); + if !matches!(resolved, Some(DynSolType::Tuple(_))) { + let ty = resolved.unwrap_or_else(|| { + // alloy accepted the ABI but can't resolve this type (a component-less + // `tuple`); emit a Bytes leaf rather than abort scaffolding. + eprintln!("warning: scaffold could not resolve type `{selector_type}`, using Bytes"); + DynSolType::Bytes + }); out.push(InputLeaf { field: field_path.join("_"), accessor: accessor_path.join("."), - solidity_type: solidity_type.to_string(), + ty, }); return; } @@ -260,7 +270,13 @@ fn flatten_into( accessor.push(raw); let mut fields = field_path.to_vec(); fields.push(field); - flatten_into(out, &accessor, &fields, &comp.ty, &comp.components); + flatten_into( + out, + &accessor, + &fields, + &comp.selector_type(), + &comp.components, + ); } } diff --git a/gnd/src/scaffold/mapping.rs b/gnd/src/scaffold/mapping.rs index af4e97ad566..2ca0103d5c5 100644 --- a/gnd/src/scaffold/mapping.rs +++ b/gnd/src/scaffold/mapping.rs @@ -1,5 +1,7 @@ //! Mapping (AssemblyScript) generation for scaffold. +use graph::abi::DynSolType; + use super::ScaffoldOptions; use super::manifest::{EventInfo, ResolvedEvent, extract_events_from_abi}; use super::sanitize_field_name; @@ -205,7 +207,7 @@ fn generate_single_handler(resolved: &ResolvedEvent) -> String { // param for unnamed). let mut field_assignments = String::new(); for leaf in super::flatten_event_inputs(&resolved.event.inputs) { - if needs_bytes_array_cast(&leaf.solidity_type) { + if needs_bytes_array_cast(&leaf.ty) { field_assignments.push_str(&format!( " entity.{} = changetype(event.params.{})\n", leaf.field, leaf.accessor @@ -242,8 +244,8 @@ fn generate_single_handler(resolved: &ResolvedEvent) -> String { /// `tuple[]` would be an unchecked reinterpret that compiles and then writes /// heap pointers into the entity. It has no `Bytes[]` form, so it gets no cast /// and fails to compile instead. -fn needs_bytes_array_cast(solidity_type: &str) -> bool { - solidity_type == "address[]" +fn needs_bytes_array_cast(ty: &DynSolType) -> bool { + matches!(ty, DynSolType::Array(inner) if matches!(**inner, DynSolType::Address)) } /// Extract callable functions from ABI for documentation comments. diff --git a/gnd/src/scaffold/schema.rs b/gnd/src/scaffold/schema.rs index 8220c7ac252..ab8198ba2a9 100644 --- a/gnd/src/scaffold/schema.rs +++ b/gnd/src/scaffold/schema.rs @@ -45,10 +45,16 @@ fn generate_example_entity(inputs: &[EventParam]) -> String { // Include first 2 event params with type comments for input in inputs.iter().take(2) { let field_name = sanitize_field_name(&input.name); - let graphql_type = solidity_to_graphql(&input.ty); + // Resolve from `selector_type` (a tuple's `ty` is the bare "tuple"), but + // keep the `# {ty}` comment on the raw declared type. Bytes is the + // fallback for a type alloy can't resolve. + let ty = crate::abi::try_resolve_selector_type(&input.selector_type()) + .unwrap_or(DynSolType::Bytes); fields.push_str(&format!( " {}: {}! # {}\n", - field_name, graphql_type, input.ty + field_name, + graphql_type(&ty), + input.ty )); } @@ -68,8 +74,7 @@ pub fn generate_event_entity(entity_name: &str, inputs: &[EventParam]) -> String // Fields from event inputs (tuples are unrolled into a field per component). for leaf in super::flatten_event_inputs(inputs) { - let graphql_type = solidity_to_graphql(&leaf.solidity_type); - fields.push_str(&format!(" {}: {}!\n", leaf.field, graphql_type)); + fields.push_str(&format!(" {}: {}!\n", leaf.field, graphql_type(&leaf.ty))); } // Standard blockchain fields @@ -84,64 +89,49 @@ pub fn generate_event_entity(entity_name: &str, inputs: &[EventParam]) -> String ) } -/// Convert Solidity type to GraphQL type. -fn solidity_to_graphql(solidity_type: &str) -> &'static str { - // Handle arrays - if solidity_type.ends_with("[]") { - let inner = &solidity_type.strip_suffix("[]").unwrap(); - return match solidity_to_graphql(inner) { - "Bytes" => "[Bytes!]", - "BigInt" => "[BigInt!]", - "Int" => "[Int!]", - // Mid-size int arrays stay `[BigInt!]`: `ethereum.Value` has no i64 - // array accessor, so `Int8` applies to scalar ints only. Matches the - // codegen, which decodes int arrays of this band via `toBigIntArray()` - // (see codegen/abi.rs). - "Int8" => "[BigInt!]", - "String" => "[String!]", - "Boolean" => "[Boolean!]", - _ => "[Bytes!]", - }; +/// Map a resolved Solidity type to the GraphQL scalar the scaffold emits. +/// +/// The match is exhaustive on purpose: enabling alloy's `eip712` feature adds a +/// `CustomStruct` variant, and a missing arm should fail to compile rather than +/// silently fall through. +fn graphql_type(ty: &DynSolType) -> &'static str { + match ty { + DynSolType::Address | DynSolType::Bytes | DynSolType::FixedBytes(_) => "Bytes", + // A Solidity `function` (bytes24). codegen decodes it as `ethereum.Tuple` + // (`.toTuple()`), so schema and binding disagree; kept as the pre-existing + // scaffold behavior, tracked in #6684. + DynSolType::Function => "Bytes", + DynSolType::Bool => "Boolean", + DynSolType::String => "String", + DynSolType::Int(bits) => int_scalar(true, *bits as u32), + DynSolType::Uint(bits) => int_scalar(false, *bits as u32), + // An indexed reference type arrives as a `bytes32` hash leaf, and a + // placeholder tuple renders opaque: either way one `Bytes` field. + DynSolType::Tuple(_) => "Bytes", + DynSolType::Array(inner) | DynSolType::FixedArray(inner, _) => list_of(inner), } +} - match solidity_type { - // Address types - "address" => "Bytes", - - // Boolean - "bool" => "Boolean", - - // String - "string" => "String", - - // Bytes types - "bytes" => "Bytes", - "bytes1" | "bytes2" | "bytes3" | "bytes4" | "bytes5" | "bytes6" | "bytes7" | "bytes8" - | "bytes9" | "bytes10" | "bytes11" | "bytes12" | "bytes13" | "bytes14" | "bytes15" - | "bytes16" | "bytes17" | "bytes18" | "bytes19" | "bytes20" | "bytes21" | "bytes22" - | "bytes23" | "bytes24" | "bytes25" | "bytes26" | "bytes27" | "bytes28" | "bytes29" - | "bytes30" | "bytes31" | "bytes32" => "Bytes", - - // Integers: small widths fit in an i32 (GraphQL Int), the rest need BigInt. - t if t.starts_with("uint") || t.starts_with("int") => int_to_graphql(t), - - // Default to Bytes for unknown types - _ => "Bytes", +/// The GraphQL list type for an array element. The `Int8` (i64) band collapses to +/// `[BigInt!]`: `ethereum.Value` has no i64 array accessor, so the codegen decodes +/// int arrays of that band via `toBigIntArray()` (see codegen/abi.rs). Nested +/// lists flatten to one dimension, matching graph-node's store, which has none. +fn list_of(inner: &DynSolType) -> &'static str { + match graphql_type(inner) { + "Int" => "[Int!]", + "Boolean" => "[Boolean!]", + "String" => "[String!]", + "Int8" | "BigInt" => "[BigInt!]", + // Bytes, and any nested list degraded to one dimension. + _ => "[Bytes!]", } } -/// Map a Solidity integer type to the narrowest GraphQL scalar that holds it: -/// `Int` (i32), `Int8` (i64), or `BigInt`. Parses the type with alloy (the same -/// parser the codegen uses) and classifies the width; see -/// [`classify_int_width`] for the cutoffs. Anything that isn't an integer type -/// falls back to `BigInt`. -fn int_to_graphql(solidity_type: &str) -> &'static str { - let (signed, bits) = match solidity_type.parse::() { - Ok(DynSolType::Int(bits)) => (true, bits as u32), - Ok(DynSolType::Uint(bits)) => (false, bits as u32), - _ => return "BigInt", - }; - +/// Map a Solidity integer to the narrowest GraphQL scalar that holds it: +/// `Int` (i32), `Int8` (i64), or `BigInt`; see [`classify_int_width`] for the +/// cutoffs. Must match codegen's `asc_type_for_ethereum`, or the schema and the +/// generated bindings disagree. +fn int_scalar(signed: bool, bits: u32) -> &'static str { match classify_int_width(signed, bits) { IntWidth::I32 => "Int", IntWidth::I64 => "Int8", @@ -247,40 +237,47 @@ mod tests { assert!(schema.contains("value: BigInt!")); } + /// Resolve a Solidity type string and map it, exercising the same path the + /// generators use. + fn graphql_type_of(solidity_type: &str) -> &'static str { + graphql_type(&solidity_type.parse::().unwrap()) + } + #[test] - fn test_solidity_to_graphql() { - assert_eq!(solidity_to_graphql("address"), "Bytes"); - assert_eq!(solidity_to_graphql("bool"), "Boolean"); - assert_eq!(solidity_to_graphql("string"), "String"); - assert_eq!(solidity_to_graphql("bytes32"), "Bytes"); - assert_eq!(solidity_to_graphql("bytes"), "Bytes"); - assert_eq!(solidity_to_graphql("address[]"), "[Bytes!]"); - assert_eq!(solidity_to_graphql("uint256[]"), "[BigInt!]"); + fn test_graphql_type() { + assert_eq!(graphql_type_of("address"), "Bytes"); + assert_eq!(graphql_type_of("bool"), "Boolean"); + assert_eq!(graphql_type_of("string"), "String"); + assert_eq!(graphql_type_of("bytes32"), "Bytes"); + assert_eq!(graphql_type_of("bytes"), "Bytes"); + assert_eq!(graphql_type_of("address[]"), "[Bytes!]"); + assert_eq!(graphql_type_of("uint256[]"), "[BigInt!]"); } #[test] fn test_integer_width_mapping() { // Widths that fit an i32 -> Int. - assert_eq!(solidity_to_graphql("int8"), "Int"); - assert_eq!(solidity_to_graphql("int32"), "Int"); - assert_eq!(solidity_to_graphql("uint8"), "Int"); - assert_eq!(solidity_to_graphql("uint24"), "Int"); + assert_eq!(graphql_type_of("int8"), "Int"); + assert_eq!(graphql_type_of("int32"), "Int"); + assert_eq!(graphql_type_of("uint8"), "Int"); + assert_eq!(graphql_type_of("uint24"), "Int"); // Wider widths that still fit an i64 -> Int8. - assert_eq!(solidity_to_graphql("uint32"), "Int8"); // first unsigned to overflow i32 - assert_eq!(solidity_to_graphql("int40"), "Int8"); - assert_eq!(solidity_to_graphql("int64"), "Int8"); - assert_eq!(solidity_to_graphql("uint56"), "Int8"); // largest unsigned that fits i64 + assert_eq!(graphql_type_of("uint32"), "Int8"); // first unsigned to overflow i32 + assert_eq!(graphql_type_of("int40"), "Int8"); + assert_eq!(graphql_type_of("int64"), "Int8"); + assert_eq!(graphql_type_of("uint56"), "Int8"); // largest unsigned that fits i64 // Too wide for i64 -> BigInt. - assert_eq!(solidity_to_graphql("int72"), "BigInt"); - assert_eq!(solidity_to_graphql("uint64"), "BigInt"); // 2^64-1 overflows i64 - assert_eq!(solidity_to_graphql("uint256"), "BigInt"); - assert_eq!(solidity_to_graphql("int"), "BigInt"); - assert_eq!(solidity_to_graphql("uint"), "BigInt"); + assert_eq!(graphql_type_of("int72"), "BigInt"); + assert_eq!(graphql_type_of("uint64"), "BigInt"); // 2^64-1 overflows i64 + assert_eq!(graphql_type_of("uint256"), "BigInt"); + // Bare `uint`/`int` canonicalize to the 256-bit form. + assert_eq!(graphql_type_of("int"), "BigInt"); + assert_eq!(graphql_type_of("uint"), "BigInt"); // Arrays: the i32 band keeps Int; the Int8 band collapses to BigInt (no // i64 array accessor), and wider stays BigInt. - assert_eq!(solidity_to_graphql("int8[]"), "[Int!]"); - assert_eq!(solidity_to_graphql("int40[]"), "[BigInt!]"); - assert_eq!(solidity_to_graphql("uint64[]"), "[BigInt!]"); + assert_eq!(graphql_type_of("int8[]"), "[Int!]"); + assert_eq!(graphql_type_of("int40[]"), "[BigInt!]"); + assert_eq!(graphql_type_of("uint64[]"), "[BigInt!]"); } #[test] From 751ba06ee0469feda2383a7110b7cb5dc0dfe19c Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Tue, 21 Jul 2026 19:02:20 +0530 Subject: [PATCH 2/3] gnd: Cast fixed-size address arrays to Bytes[] in scaffold A non-indexed `address[N]` resolves to `FixedArray(Address, N)` and now maps to a `[Bytes!]` schema field, but the mapping left `entity.x = event.params.x` uncast, assigning `Array
` to a `Bytes[]` field and failing the build. Match `FixedArray(Address, _)` alongside `Array(Address)` so the `changetype` covers both. Safe because an indexed address array is already reduced to a `bytes32` hash leaf before it reaches this cast. --- gnd/src/scaffold/mapping.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/gnd/src/scaffold/mapping.rs b/gnd/src/scaffold/mapping.rs index 2ca0103d5c5..430e2097d1a 100644 --- a/gnd/src/scaffold/mapping.rs +++ b/gnd/src/scaffold/mapping.rs @@ -236,16 +236,25 @@ fn generate_single_handler(resolved: &ResolvedEvent) -> String { ) } -/// Whether a leaf is an `address[]`, whose binding type `Array
` needs a -/// `changetype` to fit a `Bytes[]` entity field. +/// Whether a leaf is an address array (`address[]` or `address[N]`), whose +/// binding type `Array
` needs a `changetype` to fit a `Bytes[]` entity +/// field. /// /// Only `address` qualifies: `Address extends Bytes`, so the cast is an upcast. /// `ethereum.Tuple` extends `Array` and is not a `Bytes`, so casting a /// `tuple[]` would be an unchecked reinterpret that compiles and then writes /// heap pointers into the entity. It has no `Bytes[]` form, so it gets no cast /// and fails to compile instead. +/// +/// An indexed address array reaches the entity as a `bytes32` hash leaf, not an +/// array (`flatten_event_inputs` short-circuits it), so it never gets here and +/// this cast cannot reinterpret a hash. fn needs_bytes_array_cast(ty: &DynSolType) -> bool { - matches!(ty, DynSolType::Array(inner) if matches!(**inner, DynSolType::Address)) + matches!( + ty, + DynSolType::Array(inner) | DynSolType::FixedArray(inner, _) + if matches!(**inner, DynSolType::Address) + ) } /// Extract callable functions from ABI for documentation comments. @@ -524,6 +533,20 @@ mod tests { ); } + #[test] + fn test_needs_bytes_array_cast() { + let cast = |t: &str| needs_bytes_array_cast(&t.parse::().unwrap()); + // Both dynamic and fixed-size address arrays need the upcast to Bytes[]. + assert!(cast("address[]")); + assert!(cast("address[3]")); + // Nothing else does: a plain address, a non-address array, or a tuple + // array (which has no Bytes[] form and must fail to compile instead). + assert!(!cast("address")); + assert!(!cast("uint256[]")); + assert!(!cast("uint256[3]")); + assert!(!cast("(address,uint256)[]")); + } + #[test] fn test_generate_mapping_indexed_reference_params_are_hashes() { // Indexed reference types are keccak'd into a topic, so the binding From f5e1cf4c501db9bf156144dc5d8aca3bbd33978b Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Tue, 21 Jul 2026 19:04:19 +0530 Subject: [PATCH 3/3] gnd: Test scaffold type mappings across scalars and arrays Cover every schema-side type-mapping row in one event, and the mapping cast for a fixed-size address array end-to-end. The fixed-size array rows (`address[3]`, `uint256[3]`, `uint8[3]`, `bool[2]`, `string[2]`, `bytes32[4]`) regress-guard the string matcher that could not see `[N]` and scaffolded them to non-building fields. --- gnd/src/scaffold/manifest.rs | 17 ++++++++++ gnd/src/scaffold/mapping.rs | 35 +++++++++++++++++++++ gnd/src/scaffold/schema.rs | 61 ++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/gnd/src/scaffold/manifest.rs b/gnd/src/scaffold/manifest.rs index 83d08b56ca0..aad26eba2de 100644 --- a/gnd/src/scaffold/manifest.rs +++ b/gnd/src/scaffold/manifest.rs @@ -623,4 +623,21 @@ mod tests { let fields: Vec<&str> = leaves.iter().map(|l| l.field.as_str()).collect(); assert_eq!(fields, vec!["value", "value2", "value1"]); } + + #[test] + fn test_flatten_component_less_tuple_falls_back_to_bytes() { + // alloy accepts a `tuple` with no components (solc never emits one), whose + // selector type is the bare "tuple" and does not resolve. Scaffolding must + // fall back to a single Bytes leaf, not panic like codegen's resolver. + let input = EventParam { + name: "data".to_string(), + ty: "tuple".to_string(), + components: vec![], + ..Default::default() + }; + let leaves = flatten_event_inputs(&[input]); + assert_eq!(leaves.len(), 1); + assert_eq!(leaves[0].field, "data"); + assert_eq!(leaves[0].ty, DynSolType::Bytes); + } } diff --git a/gnd/src/scaffold/mapping.rs b/gnd/src/scaffold/mapping.rs index 430e2097d1a..77c1e6b620d 100644 --- a/gnd/src/scaffold/mapping.rs +++ b/gnd/src/scaffold/mapping.rs @@ -533,6 +533,41 @@ mod tests { ); } + #[test] + fn test_generate_mapping_casts_fixed_size_address_array() { + // A non-indexed `address[N]` reaches the entity as an array, so it needs + // the same upcast as `address[]`; a fixed uint array does not. + let abi = json!([ + { + "type": "event", + "name": "Signers", + "inputs": [ + {"name": "owners", "type": "address[3]"}, + {"name": "amounts", "type": "uint256[3]"} + ] + } + ]); + + let options = ScaffoldOptions { + contract_name: "Vault".to_string(), + abi: Some(abi), + index_events: true, + ..Default::default() + }; + + let mapping = generate_mapping(&options); + assert!( + mapping.contains("entity.owners = changetype(event.params.owners)"), + "{}", + mapping + ); + assert!( + mapping.contains("entity.amounts = event.params.amounts\n"), + "{}", + mapping + ); + } + #[test] fn test_needs_bytes_array_cast() { let cast = |t: &str| needs_bytes_array_cast(&t.parse::().unwrap()); diff --git a/gnd/src/scaffold/schema.rs b/gnd/src/scaffold/schema.rs index ab8198ba2a9..20835525a41 100644 --- a/gnd/src/scaffold/schema.rs +++ b/gnd/src/scaffold/schema.rs @@ -280,6 +280,67 @@ mod tests { assert_eq!(graphql_type_of("uint64[]"), "[BigInt!]"); } + #[test] + fn test_generate_schema_covers_all_type_mappings() { + // One event exercising every schema-side mapping row, including the + // fixed-size arrays the old `ends_with("[]")` matcher could not see and + // that scaffolded to non-building scalar fields. + let param = |name: &str, ty: &str| json!({"name": name, "type": ty, "indexed": false}); + let abi = json!([ + { + "type": "event", + "name": "AllTypes", + "inputs": [ + param("addr", "address"), + param("flag", "bool"), + param("text", "string"), + param("tiny", "uint8"), // i32 band + param("mid", "uint32"), // i64 band + param("big", "uint256"), // BigInt + param("addrsDyn", "address[]"), + param("addrsFixed", "address[3]"), + param("numsFixed", "uint256[3]"), + param("tinyFixed", "uint8[3]"), + param("flagsFixed", "bool[2]"), + param("textFixed", "string[2]"), + param("hashesFixed", "bytes32[4]"), + param("tinyDyn", "int8[]"), // i32 band keeps Int + param("midDyn", "int40[]"), // i64 band collapses to BigInt + ] + } + ]); + + let options = ScaffoldOptions { + abi: Some(abi), + index_events: true, + ..Default::default() + }; + let schema = generate_schema(&options); + + for (field, ty) in [ + ("addr", "Bytes!"), + ("flag", "Boolean!"), + ("text", "String!"), + ("tiny", "Int!"), + ("mid", "Int8!"), + ("big", "BigInt!"), + ("addrsDyn", "[Bytes!]!"), + ("addrsFixed", "[Bytes!]!"), + ("numsFixed", "[BigInt!]!"), + ("tinyFixed", "[Int!]!"), + ("flagsFixed", "[Boolean!]!"), + ("textFixed", "[String!]!"), + ("hashesFixed", "[Bytes!]!"), + ("tinyDyn", "[Int!]!"), + ("midDyn", "[BigInt!]!"), + ] { + assert!( + schema.contains(&format!("{field}: {ty}")), + "expected `{field}: {ty}` in:\n{schema}" + ); + } + } + #[test] fn test_generate_schema_unrolls_tuple() { let abi = json!([