Skip to content

Commit ac82dda

Browse files
fubhyclaude
authored andcommitted
core: Implement struct field access in declarative calls (spec v1.4.0)
This commit implements support for struct field access in declarative calls, allowing subgraphs to access nested struct fields using dot notation. Key changes: - Add struct field access parsing and validation - Support arbitrary nesting depth for field access - Update subgraph manifest documentation - Add comprehensive tests for struct field access functionality - Update API version to support new feature - Initial deserialization stores calls as raw strings in UnresolvedCallDecls - Resolution phase parses calls with ABI context using CallExpr::parse() - Spec version validation happens during parsing with clear error messages - Add UnresolvedCallDecls struct to store raw call strings - Add UnresolvedMappingEventHandler and UnresolvedEntityHandler - Move call parsing from deserialization to resolution phase - Replace eprintln! with proper logger.warn() calls - Remove obsolete validation functions - Add a justfile target to recompile test contracts - Extract their ABIs into tests/contracts/abi/ - Add integration tests for using struct fields in declared calls Co-Authored-By: Claude <[email protected]> Co-Authored-By: Sebastian Lorenz <[email protected]>
1 parent ad3cd9e commit ac82dda

File tree

39 files changed

+3638
-167
lines changed

39 files changed

+3638
-167
lines changed

chain/ethereum/src/data_source.rs

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use graph::components::store::{EthereumCallCache, StoredDynamicDataSource};
66
use graph::components::subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError};
77
use graph::components::trigger_processor::RunnableTriggers;
88
use graph::data_source::common::{
9-
CallDecls, DeclaredCall, FindMappingABI, MappingABI, UnresolvedMappingABI,
9+
AbiJson, CallDecls, DeclaredCall, FindMappingABI, MappingABI, UnresolvedCallDecls,
10+
UnresolvedMappingABI,
1011
};
1112
use graph::data_source::{CausalityRegion, MappingTrigger as MappingTriggerType};
1213
use graph::env::ENV_VARS;
@@ -800,7 +801,7 @@ impl DataSource {
800801
"transaction" => format!("{}", &transaction.hash),
801802
});
802803
let handler = event_handler.handler.clone();
803-
let calls = DeclaredCall::from_log_trigger(
804+
let calls = DeclaredCall::from_log_trigger_with_event(
804805
&self.mapping,
805806
&event_handler.calls,
806807
&log,
@@ -1200,6 +1201,7 @@ impl blockchain::UnresolvedDataSource<Chain> for UnresolvedDataSource {
12001201
resolver: &Arc<dyn LinkResolver>,
12011202
logger: &Logger,
12021203
manifest_idx: u32,
1204+
spec_version: &semver::Version,
12031205
) -> Result<DataSource, anyhow::Error> {
12041206
let UnresolvedDataSource {
12051207
kind,
@@ -1210,7 +1212,7 @@ impl blockchain::UnresolvedDataSource<Chain> for UnresolvedDataSource {
12101212
context,
12111213
} = self;
12121214

1213-
let mapping = mapping.resolve(resolver, logger).await.with_context(|| {
1215+
let mapping = mapping.resolve(resolver, logger, spec_version).await.with_context(|| {
12141216
format!(
12151217
"failed to resolve data source {} with source_address {:?} and source_start_block {}",
12161218
name, source.address, source.start_block
@@ -1221,7 +1223,7 @@ impl blockchain::UnresolvedDataSource<Chain> for UnresolvedDataSource {
12211223
}
12221224
}
12231225

1224-
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Deserialize)]
1226+
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
12251227
pub struct UnresolvedDataSourceTemplate {
12261228
pub kind: String,
12271229
pub network: Option<String>,
@@ -1247,6 +1249,7 @@ impl blockchain::UnresolvedDataSourceTemplate<Chain> for UnresolvedDataSourceTem
12471249
resolver: &Arc<dyn LinkResolver>,
12481250
logger: &Logger,
12491251
manifest_idx: u32,
1252+
spec_version: &semver::Version,
12501253
) -> Result<DataSourceTemplate, anyhow::Error> {
12511254
let UnresolvedDataSourceTemplate {
12521255
kind,
@@ -1257,7 +1260,7 @@ impl blockchain::UnresolvedDataSourceTemplate<Chain> for UnresolvedDataSourceTem
12571260
} = self;
12581261

12591262
let mapping = mapping
1260-
.resolve(resolver, logger)
1263+
.resolve(resolver, logger, spec_version)
12611264
.await
12621265
.with_context(|| format!("failed to resolve data source template {}", name))?;
12631266

@@ -1294,7 +1297,7 @@ impl blockchain::DataSourceTemplate<Chain> for DataSourceTemplate {
12941297
}
12951298
}
12961299

1297-
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Deserialize)]
1300+
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
12981301
#[serde(rename_all = "camelCase")]
12991302
pub struct UnresolvedMapping {
13001303
pub kind: String,
@@ -1307,7 +1310,7 @@ pub struct UnresolvedMapping {
13071310
#[serde(default)]
13081311
pub call_handlers: Vec<MappingCallHandler>,
13091312
#[serde(default)]
1310-
pub event_handlers: Vec<MappingEventHandler>,
1313+
pub event_handlers: Vec<UnresolvedMappingEventHandler>,
13111314
pub file: Link,
13121315
}
13131316

@@ -1357,6 +1360,7 @@ impl UnresolvedMapping {
13571360
self,
13581361
resolver: &Arc<dyn LinkResolver>,
13591362
logger: &Logger,
1363+
spec_version: &semver::Version,
13601364
) -> Result<Mapping, anyhow::Error> {
13611365
let UnresolvedMapping {
13621366
kind,
@@ -1376,9 +1380,7 @@ impl UnresolvedMapping {
13761380
// resolve each abi
13771381
abis.into_iter()
13781382
.map(|unresolved_abi| async {
1379-
Result::<_, Error>::Ok(Arc::new(
1380-
unresolved_abi.resolve(resolver, logger).await?,
1381-
))
1383+
Result::<_, Error>::Ok(unresolved_abi.resolve(resolver, logger).await?)
13821384
})
13831385
.collect::<FuturesOrdered<_>>()
13841386
.try_collect::<Vec<_>>(),
@@ -1390,15 +1392,35 @@ impl UnresolvedMapping {
13901392
.await
13911393
.with_context(|| format!("failed to resolve mapping {}", link.link))?;
13921394

1395+
// Resolve event handlers with ABI context
1396+
let resolved_event_handlers = event_handlers
1397+
.into_iter()
1398+
.map(|unresolved_handler| {
1399+
// Find the ABI for this event handler
1400+
let (_, abi_json) = abis.first().ok_or_else(|| {
1401+
anyhow!(
1402+
"No ABI found for event '{}' in event handler '{}'",
1403+
unresolved_handler.event,
1404+
unresolved_handler.handler
1405+
)
1406+
})?;
1407+
1408+
unresolved_handler.resolve(abi_json, &spec_version)
1409+
})
1410+
.collect::<Result<Vec<_>, anyhow::Error>>()?;
1411+
1412+
// Extract just the MappingABIs for the final Mapping struct
1413+
let mapping_abis = abis.into_iter().map(|(abi, _)| Arc::new(abi)).collect();
1414+
13931415
Ok(Mapping {
13941416
kind,
13951417
api_version,
13961418
language,
13971419
entities,
1398-
abis,
1420+
abis: mapping_abis,
13991421
block_handlers: block_handlers.clone(),
14001422
call_handlers: call_handlers.clone(),
1401-
event_handlers: event_handlers.clone(),
1423+
event_handlers: resolved_event_handlers,
14021424
runtime,
14031425
link,
14041426
})
@@ -1442,8 +1464,8 @@ pub struct MappingCallHandler {
14421464
pub handler: String,
14431465
}
14441466

1445-
#[derive(Clone, Debug, Hash, Eq, PartialEq, Deserialize)]
1446-
pub struct MappingEventHandler {
1467+
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
1468+
pub struct UnresolvedMappingEventHandler {
14471469
pub event: String,
14481470
pub topic0: Option<H256>,
14491471
#[serde(deserialize_with = "deserialize_h256_vec", default)]
@@ -1456,6 +1478,41 @@ pub struct MappingEventHandler {
14561478
#[serde(default)]
14571479
pub receipt: bool,
14581480
#[serde(default)]
1481+
pub calls: UnresolvedCallDecls,
1482+
}
1483+
1484+
impl UnresolvedMappingEventHandler {
1485+
pub fn resolve(
1486+
self,
1487+
abi_json: &AbiJson,
1488+
spec_version: &semver::Version,
1489+
) -> Result<MappingEventHandler, anyhow::Error> {
1490+
let resolved_calls = self
1491+
.calls
1492+
.resolve(abi_json, Some(&self.event), spec_version)?;
1493+
1494+
Ok(MappingEventHandler {
1495+
event: self.event,
1496+
topic0: self.topic0,
1497+
topic1: self.topic1,
1498+
topic2: self.topic2,
1499+
topic3: self.topic3,
1500+
handler: self.handler,
1501+
receipt: self.receipt,
1502+
calls: resolved_calls,
1503+
})
1504+
}
1505+
}
1506+
1507+
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
1508+
pub struct MappingEventHandler {
1509+
pub event: String,
1510+
pub topic0: Option<H256>,
1511+
pub topic1: Option<Vec<H256>>,
1512+
pub topic2: Option<Vec<H256>>,
1513+
pub topic3: Option<Vec<H256>>,
1514+
pub handler: String,
1515+
pub receipt: bool,
14591516
pub calls: CallDecls,
14601517
}
14611518

chain/near/src/data_source.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ impl blockchain::UnresolvedDataSource<Chain> for UnresolvedDataSource {
333333
resolver: &Arc<dyn LinkResolver>,
334334
logger: &Logger,
335335
_manifest_idx: u32,
336+
_spec_version: &semver::Version,
336337
) -> Result<DataSource, Error> {
337338
let UnresolvedDataSource {
338339
kind,
@@ -372,6 +373,7 @@ impl blockchain::UnresolvedDataSourceTemplate<Chain> for UnresolvedDataSourceTem
372373
resolver: &Arc<dyn LinkResolver>,
373374
logger: &Logger,
374375
_manifest_idx: u32,
376+
_spec_version: &semver::Version,
375377
) -> Result<DataSourceTemplate, Error> {
376378
let UnresolvedDataSourceTemplate {
377379
kind,

chain/substreams/src/data_source.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ impl blockchain::UnresolvedDataSource<Chain> for UnresolvedDataSource {
187187
resolver: &Arc<dyn LinkResolver>,
188188
logger: &Logger,
189189
_manifest_idx: u32,
190+
_spec_version: &semver::Version,
190191
) -> Result<DataSource, Error> {
191192
let content = resolver.cat(logger, &self.source.package.file).await?;
192193

@@ -317,6 +318,7 @@ impl blockchain::UnresolvedDataSourceTemplate<Chain> for NoopDataSourceTemplate
317318
_resolver: &Arc<dyn LinkResolver>,
318319
_logger: &Logger,
319320
_manifest_idx: u32,
321+
_spec_version: &semver::Version,
320322
) -> Result<NoopDataSourceTemplate, anyhow::Error> {
321323
unimplemented!("{}", TEMPLATE_ERROR)
322324
}
@@ -330,7 +332,7 @@ mod test {
330332
use graph::{
331333
blockchain::{DataSource as _, UnresolvedDataSource as _},
332334
components::link_resolver::LinkResolver,
333-
data::subgraph::LATEST_VERSION,
335+
data::subgraph::{LATEST_VERSION, SPEC_VERSION_1_2_0},
334336
prelude::{async_trait, serde_yaml, JsonValueStream, Link},
335337
slog::{o, Discard, Logger},
336338
substreams::{
@@ -433,7 +435,10 @@ mod test {
433435
let ds: UnresolvedDataSource = serde_yaml::from_str(TEMPLATE_DATA_SOURCE).unwrap();
434436
let link_resolver: Arc<dyn LinkResolver> = Arc::new(NoopLinkResolver {});
435437
let logger = Logger::root(Discard, o!());
436-
let ds: DataSource = ds.resolve(&link_resolver, &logger, 0).await.unwrap();
438+
let ds: DataSource = ds
439+
.resolve(&link_resolver, &logger, 0, &SPEC_VERSION_1_2_0)
440+
.await
441+
.unwrap();
437442
let expected = DataSource {
438443
kind: SUBSTREAMS_KIND.into(),
439444
network: Some("mainnet".into()),
@@ -470,7 +475,10 @@ mod test {
470475
serde_yaml::from_str(TEMPLATE_DATA_SOURCE_WITH_PARAMS).unwrap();
471476
let link_resolver: Arc<dyn LinkResolver> = Arc::new(NoopLinkResolver {});
472477
let logger = Logger::root(Discard, o!());
473-
let ds: DataSource = ds.resolve(&link_resolver, &logger, 0).await.unwrap();
478+
let ds: DataSource = ds
479+
.resolve(&link_resolver, &logger, 0, &SPEC_VERSION_1_2_0)
480+
.await
481+
.unwrap();
474482
let expected = DataSource {
475483
kind: SUBSTREAMS_KIND.into(),
476484
network: Some("mainnet".into()),

docs/subgraph-manifest.md

Lines changed: 12 additions & 2 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
@@ -118,7 +118,17 @@ Each call is of the form `<ABI>[<address>].<function>(<args>)`:
118118
| **function** | *String* | The name of a view function in the contract |
119119
| **args** | *[Expr]* | The arguments to pass to the function |
120120

121-
The `Expr` can be either `event.address` or `event.params.<name>`.
121+
#### Expression Types
122+
123+
The `Expr` can be one of the following:
124+
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+) |
131+
122132

123133
## 1.6 Path
124134
A path has one field `path`, which either refers to a path of a file on the local dev machine or an [IPLD link](https://github.com/ipld/specs/).

graph/src/blockchain/mock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ impl<C: Blockchain> UnresolvedDataSource<C> for MockUnresolvedDataSource {
193193
_resolver: &Arc<dyn LinkResolver>,
194194
_logger: &slog::Logger,
195195
_manifest_idx: u32,
196+
_spec_version: &semver::Version,
196197
) -> Result<C::DataSource, anyhow::Error> {
197198
todo!()
198199
}
@@ -243,6 +244,7 @@ impl<C: Blockchain> UnresolvedDataSourceTemplate<C> for MockUnresolvedDataSource
243244
_resolver: &Arc<dyn LinkResolver>,
244245
_logger: &slog::Logger,
245246
_manifest_idx: u32,
247+
_spec_version: &semver::Version,
246248
) -> Result<C::DataSourceTemplate, anyhow::Error> {
247249
todo!()
248250
}

graph/src/blockchain/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ pub trait UnresolvedDataSourceTemplate<C: Blockchain>:
378378
resolver: &Arc<dyn LinkResolver>,
379379
logger: &Logger,
380380
manifest_idx: u32,
381+
spec_version: &semver::Version,
381382
) -> Result<C::DataSourceTemplate, anyhow::Error>;
382383
}
383384

@@ -407,6 +408,7 @@ pub trait UnresolvedDataSource<C: Blockchain>:
407408
resolver: &Arc<dyn LinkResolver>,
408409
logger: &Logger,
409410
manifest_idx: u32,
411+
spec_version: &semver::Version,
410412
) -> Result<C::DataSource, anyhow::Error>;
411413
}
412414

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

graph/src/data/subgraph/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,14 +1074,20 @@ impl<C: Blockchain> UnresolvedSubgraphManifest<C> {
10741074
data_sources
10751075
.into_iter()
10761076
.enumerate()
1077-
.map(|(idx, ds)| ds.resolve(resolver, logger, idx as u32))
1077+
.map(|(idx, ds)| ds.resolve(resolver, logger, idx as u32, &spec_version))
10781078
.collect::<FuturesOrdered<_>>()
10791079
.try_collect::<Vec<_>>(),
10801080
templates
10811081
.into_iter()
10821082
.enumerate()
10831083
.map(|(idx, template)| {
1084-
template.resolve(resolver, &schema, logger, ds_count as u32 + idx as u32)
1084+
template.resolve(
1085+
resolver,
1086+
&schema,
1087+
logger,
1088+
ds_count as u32 + idx as u32,
1089+
&spec_version,
1090+
)
10851091
})
10861092
.collect::<FuturesOrdered<_>>()
10871093
.try_collect::<Vec<_>>(),

0 commit comments

Comments
 (0)