Skip to content

Commit e4d12b3

Browse files
fubhyclaude
andcommitted
core: Implement struct field access in declarative calls (spec v1.4.0)
Adds named field access support for struct parameters in declarative calls (e.g., event.params.asset.addr). Field names are resolved to indices at parse time using ABI context for improved performance and user experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 966c26b commit e4d12b3

5 files changed

Lines changed: 542 additions & 429 deletions

File tree

chain/ethereum/src/data_source.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use graph::{
4141

4242
use graph::data::subgraph::{
4343
calls_host_fn, DataSourceContext, Source, MIN_SPEC_VERSION, SPEC_VERSION_0_0_8,
44-
SPEC_VERSION_1_2_0,
44+
SPEC_VERSION_1_2_0, SPEC_VERSION_1_4_0,
4545
};
4646

4747
use crate::adapter::EthereumAdapter as _;
@@ -74,6 +74,38 @@ pub struct DataSource {
7474
pub contract_abi: Arc<MappingABI>,
7575
}
7676

77+
/// Checks if a declarative call uses struct field access that would require spec version 1.4.0+
78+
/// This detects if the call was parsed with ABI context to resolve named fields
79+
fn call_uses_named_field_access(call_expr: &graph::data_source::common::CallExpr) -> bool {
80+
// Check address for struct field access
81+
if has_struct_field_access(&call_expr.address) {
82+
return true;
83+
}
84+
85+
// Check all arguments for struct field access
86+
for arg in &call_expr.args {
87+
if has_struct_field_access(arg) {
88+
return true;
89+
}
90+
}
91+
92+
false
93+
}
94+
95+
/// Helper function to check if a CallArg uses struct field access
96+
fn has_struct_field_access(arg: &graph::data_source::common::CallArg) -> bool {
97+
use graph::data_source::common::{CallArg, EthereumArg};
98+
99+
match arg {
100+
CallArg::Ethereum(EthereumArg::StructField(_, indices)) => {
101+
// If we have struct field access with indices, it means named fields were resolved
102+
// This feature requires spec version 1.4.0+
103+
!indices.is_empty()
104+
}
105+
_ => false,
106+
}
107+
}
108+
77109
impl blockchain::DataSource<Chain> for DataSource {
78110
fn from_template_info(
79111
info: InstanceDSTemplateInfo,
@@ -380,6 +412,19 @@ impl blockchain::DataSource<Chain> for DataSource {
380412
}
381413
}
382414

415+
if spec_version < &SPEC_VERSION_1_4_0 {
416+
for handler in &self.mapping.event_handlers {
417+
for call in handler.calls.decls.as_ref() {
418+
if call_uses_named_field_access(&call.expr) {
419+
errors.push(anyhow!(
420+
"handler {}: struct field access by name in declarative calls is only supported for specVersion >= 1.4.0", handler.event
421+
));
422+
break;
423+
}
424+
}
425+
}
426+
}
427+
383428
for handler in &self.mapping.event_handlers {
384429
for call in handler.calls.decls.as_ref() {
385430
match self.mapping.find_abi(&call.expr.abi) {
@@ -805,7 +850,6 @@ impl DataSource {
805850
&event_handler.calls,
806851
&log,
807852
&params,
808-
Some(&event_handler.event),
809853
)?;
810854
Ok(Some(TriggerWithHandler::<Chain>::new_with_logging_extras(
811855
MappingTrigger::Log {

docs/subgraph-manifest.md

Lines changed: 7 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ The `mapping` field may be one of the following supported mapping manifests:
9898

9999
### 1.5.3 Declaring calls
100100

101-
_Available from spec version 1.2.0_
101+
_Available from spec version 1.2.0. Struct field access available from spec version 1.4.0_
102102

103103
Declared calls are performed in parallel before the handler is run and can
104104
greatly speed up syncing. Mappings access the call results simply by using
@@ -122,76 +122,12 @@ Each call is of the form `<ABI>[<address>].<function>(<args>)`:
122122

123123
The `Expr` can be one of the following:
124124

125-
| Expression | Description | Example |
126-
| --- | --- | --- |
127-
| **event.address** | The address of the contract that emitted the event | `event.address` |
128-
| **event.params.&lt;name&gt;** | A simple parameter from the event | `event.params.token` |
129-
| **event.params.&lt;name&gt;.&lt;index&gt;** | A field from a struct parameter by numeric index | `event.params.asset.0` |
130-
| **event.params.&lt;name&gt;.&lt;fieldName&gt;** | A field from a struct parameter by field name | `event.params.asset.addr` |
131-
| **Nested struct access** | Arbitrary nesting depth with mixed access patterns | `event.params.data.1.user.id` |
132-
133-
#### Struct Field Access
134-
135-
When event parameters contain struct types (tuples in ABI), you can access individual fields using either numeric indices or field names:
136-
137-
**Numeric Access (Traditional):**
138-
```yaml
139-
calls:
140-
tokenAddress: ERC20[event.params.asset.0].name() # First field
141-
tokenAmount: ERC20[event.params.asset.1].decimals() # Second field
142-
```
143-
144-
**Named Access (Recommended):**
145-
```yaml
146-
calls:
147-
tokenAddress: ERC20[event.params.asset.addr].name() # By field name
148-
tokenAmount: ERC20[event.params.asset.amount].decimals() # By field name
149-
```
150-
151-
**Mixed Access Patterns:**
152-
```yaml
153-
calls:
154-
# Access first transfer, then recipient field by name
155-
recipient: Token[event.params.transfers.0.recipient].balanceOf()
156-
157-
# Deep nesting with mixed numeric and named access
158-
innerValue: Contract[event.params.data.1.user.addr].someFunction()
159-
```
160-
161-
#### Struct Field Access Examples
162-
163-
Given an event with this ABI structure:
164-
```json
165-
{
166-
"name": "AssetTransfer",
167-
"type": "event",
168-
"inputs": [
169-
{
170-
"name": "asset",
171-
"type": "tuple",
172-
"components": [
173-
{"name": "addr", "type": "address"},
174-
{"name": "amount", "type": "uint256"},
175-
{"name": "active", "type": "bool"}
176-
]
177-
}
178-
]
179-
}
180-
```
181-
182-
You can access fields in multiple ways:
183-
```yaml
184-
calls:
185-
# Named field access (clearest and recommended)
186-
tokenContract: ERC20[event.params.asset.addr].name()
187-
tokenDecimals: ERC20[event.params.asset.addr].decimals()
188-
189-
# Numeric field access (backward compatible)
190-
tokenContract: ERC20[event.params.asset.0].name()
191-
192-
# Mixed access for complex nested structures
193-
nestedField: Contract[event.params.data.0.inner.fieldName].process()
194-
```
125+
| Expression | Description |
126+
| --- | --- |
127+
| **event.address** | The address of the contract that emitted the event |
128+
| **event.params.&lt;name&gt;** | A simple parameter from the event |
129+
| **event.params.&lt;name&gt;.&lt;index&gt;** | A field from a struct parameter by numeric index |
130+
| **event.params.&lt;name&gt;.&lt;fieldName&gt;** | A field from a struct parameter by field name (spec version 1.4.0+) |
195131

196132

197133
## 1.6 Path

graph/src/data/subgraph/api_version.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,11 @@ pub const SPEC_VERSION_1_2_0: Version = Version::new(1, 2, 0);
6060
// represents the write order across all entity types in the subgraph.
6161
pub const SPEC_VERSION_1_3_0: Version = Version::new(1, 3, 0);
6262

63+
// Enables struct field access in declarative calls
64+
pub const SPEC_VERSION_1_4_0: Version = Version::new(1, 4, 0);
65+
6366
// The latest spec version available
64-
pub const LATEST_VERSION: &Version = &SPEC_VERSION_1_3_0;
67+
pub const LATEST_VERSION: &Version = &SPEC_VERSION_1_4_0;
6568

6669
pub const MIN_SPEC_VERSION: Version = Version::new(0, 0, 2);
6770

0 commit comments

Comments
 (0)