Skip to content

Commit f03f6d2

Browse files
fubhyclaude
andcommitted
core: Implement two-phase parsing for ABI-aware call resolution
Addresses feedback from Lutter about supporting struct field access in declarative calls in production. Previously, manifest parsing happened via serde deserialize which called FromStr without ABI context, preventing named field resolution outside of tests. This implements two-phase parsing where: - 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 Changes: - 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 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 524eb85 commit f03f6d2

File tree

3 files changed

+236
-73
lines changed

3 files changed

+236
-73
lines changed

chain/ethereum/src/data_source.rs

Lines changed: 70 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ 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+
CallDecls, DeclaredCall, FindMappingABI, MappingABI, UnresolvedCallDecls, UnresolvedMappingABI,
1010
};
1111
use graph::data_source::{CausalityRegion, MappingTrigger as MappingTriggerType};
1212
use graph::env::ENV_VARS;
@@ -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, SPEC_VERSION_1_4_0,
44+
SPEC_VERSION_1_2_0,
4545
};
4646

4747
use crate::adapter::EthereumAdapter as _;
@@ -74,38 +74,6 @@ 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-
10977
impl blockchain::DataSource<Chain> for DataSource {
11078
fn from_template_info(
11179
info: InstanceDSTemplateInfo,
@@ -412,19 +380,6 @@ impl blockchain::DataSource<Chain> for DataSource {
412380
}
413381
}
414382

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-
428383
for handler in &self.mapping.event_handlers {
429384
for call in handler.calls.decls.as_ref() {
430385
match self.mapping.find_abi(&call.expr.abi) {
@@ -1266,7 +1221,7 @@ impl blockchain::UnresolvedDataSource<Chain> for UnresolvedDataSource {
12661221
}
12671222
}
12681223

1269-
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Deserialize)]
1224+
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
12701225
pub struct UnresolvedDataSourceTemplate {
12711226
pub kind: String,
12721227
pub network: Option<String>,
@@ -1339,7 +1294,7 @@ impl blockchain::DataSourceTemplate<Chain> for DataSourceTemplate {
13391294
}
13401295
}
13411296

1342-
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Deserialize)]
1297+
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
13431298
#[serde(rename_all = "camelCase")]
13441299
pub struct UnresolvedMapping {
13451300
pub kind: String,
@@ -1352,7 +1307,7 @@ pub struct UnresolvedMapping {
13521307
#[serde(default)]
13531308
pub call_handlers: Vec<MappingCallHandler>,
13541309
#[serde(default)]
1355-
pub event_handlers: Vec<MappingEventHandler>,
1310+
pub event_handlers: Vec<UnresolvedMappingEventHandler>,
13561311
pub file: Link,
13571312
}
13581313

@@ -1435,6 +1390,30 @@ impl UnresolvedMapping {
14351390
.await
14361391
.with_context(|| format!("failed to resolve mapping {}", link.link))?;
14371392

1393+
// Resolve event handlers with ABI context
1394+
let resolved_event_handlers = event_handlers
1395+
.into_iter()
1396+
.map(|unresolved_handler| {
1397+
// Find the ABI for this event handler
1398+
let event_abi = abis
1399+
.iter()
1400+
.find(|abi| {
1401+
abi.contract
1402+
.events()
1403+
.any(|event| event.name == unresolved_handler.event)
1404+
})
1405+
.ok_or_else(|| {
1406+
anyhow!(
1407+
"No ABI found for event '{}' in event handler '{}'",
1408+
unresolved_handler.event,
1409+
unresolved_handler.handler
1410+
)
1411+
})?;
1412+
1413+
unresolved_handler.resolve(event_abi, Some(&api_version))
1414+
})
1415+
.collect::<Result<Vec<_>, anyhow::Error>>()?;
1416+
14381417
Ok(Mapping {
14391418
kind,
14401419
api_version,
@@ -1443,7 +1422,7 @@ impl UnresolvedMapping {
14431422
abis,
14441423
block_handlers: block_handlers.clone(),
14451424
call_handlers: call_handlers.clone(),
1446-
event_handlers: event_handlers.clone(),
1425+
event_handlers: resolved_event_handlers,
14471426
runtime,
14481427
link,
14491428
})
@@ -1487,6 +1466,46 @@ pub struct MappingCallHandler {
14871466
pub handler: String,
14881467
}
14891468

1469+
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
1470+
pub struct UnresolvedMappingEventHandler {
1471+
pub event: String,
1472+
pub topic0: Option<H256>,
1473+
#[serde(deserialize_with = "deserialize_h256_vec", default)]
1474+
pub topic1: Option<Vec<H256>>,
1475+
#[serde(deserialize_with = "deserialize_h256_vec", default)]
1476+
pub topic2: Option<Vec<H256>>,
1477+
#[serde(deserialize_with = "deserialize_h256_vec", default)]
1478+
pub topic3: Option<Vec<H256>>,
1479+
pub handler: String,
1480+
#[serde(default)]
1481+
pub receipt: bool,
1482+
#[serde(default)]
1483+
pub calls: UnresolvedCallDecls,
1484+
}
1485+
1486+
impl UnresolvedMappingEventHandler {
1487+
pub fn resolve(
1488+
&self,
1489+
mapping: &MappingABI,
1490+
spec_version: Option<&semver::Version>,
1491+
) -> Result<MappingEventHandler, anyhow::Error> {
1492+
let resolved_calls = self
1493+
.calls
1494+
.resolve(mapping, Some(&self.event), spec_version)?;
1495+
1496+
Ok(MappingEventHandler {
1497+
event: self.event.clone(),
1498+
topic0: self.topic0,
1499+
topic1: self.topic1.clone(),
1500+
topic2: self.topic2.clone(),
1501+
topic3: self.topic3.clone(),
1502+
handler: self.handler.clone(),
1503+
receipt: self.receipt,
1504+
calls: resolved_calls,
1505+
})
1506+
}
1507+
}
1508+
14901509
#[derive(Clone, Debug, Hash, Eq, PartialEq, Deserialize)]
14911510
pub struct MappingEventHandler {
14921511
pub event: String,

0 commit comments

Comments
 (0)