Skip to content

Commit 8b5ab6c

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 8b5ab6c

File tree

3 files changed

+229
-73
lines changed

3 files changed

+229
-73
lines changed

chain/ethereum/src/data_source.rs

Lines changed: 63 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,23 @@ 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.first().ok_or_else(|| {
1399+
anyhow!(
1400+
"No ABI found for event '{}' in event handler '{}'",
1401+
unresolved_handler.event,
1402+
unresolved_handler.handler
1403+
)
1404+
})?;
1405+
1406+
unresolved_handler.resolve(event_abi, Some(&api_version))
1407+
})
1408+
.collect::<Result<Vec<_>, anyhow::Error>>()?;
1409+
14381410
Ok(Mapping {
14391411
kind,
14401412
api_version,
@@ -1443,7 +1415,7 @@ impl UnresolvedMapping {
14431415
abis,
14441416
block_handlers: block_handlers.clone(),
14451417
call_handlers: call_handlers.clone(),
1446-
event_handlers: event_handlers.clone(),
1418+
event_handlers: resolved_event_handlers,
14471419
runtime,
14481420
link,
14491421
})
@@ -1487,6 +1459,46 @@ pub struct MappingCallHandler {
14871459
pub handler: String,
14881460
}
14891461

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

0 commit comments

Comments
 (0)