Skip to content

Commit 8a9e0f1

Browse files
gnd: replace eth_call cache with mock transport layer
Replace the pre-populated eth_call cache approach with a mock Alloy transport that intercepts RPC calls at the transport level. This makes tests exercise the real production code path end-to-end. - Add MockTransport serving eth_call, eth_getBalance, eth_getCode - Add TestRuntimeAdapterBuilder to convert PossibleReorg → Deterministic - Add getBalanceCalls/hasCodeCalls to test JSON schema - Unmocked RPC calls now fail immediately with descriptive errors - Disable RPC retries in test mode for fast failure
1 parent 919d9c0 commit 8a9e0f1

9 files changed

Lines changed: 439 additions & 90 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gnd/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ graph-store-postgres = { path = "../store/postgres" }
3131
# Test command dependencies
3232
hex = "0.4"
3333
async-trait = { workspace = true }
34+
tower = { workspace = true }
3435

3536
# Direct dependencies from current dev.rs
3637
anyhow = { workspace = true }

gnd/src/commands/test/eth_calls.rs

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
1-
//! Populates the eth_call cache with mock responses for `gnd test`.
2-
//!
3-
//! Signatures use graph-node's `name(inputs):(outputs)` convention.
4-
//! Encoding matches production graph-node so cache IDs align with the runtime.
1+
//! ABI encoding helpers for mock Ethereum call data.
52
6-
use super::schema::{MockEthCall, TestFile};
73
use super::trigger::json_to_sol_value;
84
use anyhow::{anyhow, Context, Result};
95
use graph::abi::FunctionExt as GraphFunctionExt;
10-
use graph::blockchain::block_stream::BlockWithTriggers;
11-
use graph::blockchain::BlockPtr;
12-
use graph::components::store::EthereumCallCache;
13-
use graph::data::store::ethereum::call;
146
use graph::prelude::alloy::dyn_abi::{DynSolType, FunctionExt as AlloyFunctionExt};
157
use graph::prelude::alloy::json_abi::Function;
16-
use graph::prelude::alloy::primitives::Address;
17-
use graph::slog::Logger;
18-
use graph_chain_ethereum::Chain;
19-
use graph_store_postgres::ChainStore;
20-
use std::sync::Arc;
218

229
/// ABI-encode a function call (selector + params) using graph-node's encoding path.
23-
fn encode_function_call(function_sig: &str, params: &[serde_json::Value]) -> Result<Vec<u8>> {
10+
pub(super) fn encode_function_call(
11+
function_sig: &str,
12+
params: &[serde_json::Value],
13+
) -> Result<Vec<u8>> {
2414
let alloy_sig = to_alloy_signature(function_sig);
2515
let function = Function::parse(&alloy_sig).map_err(|e| {
2616
anyhow!(
@@ -55,7 +45,10 @@ fn encode_function_call(function_sig: &str, params: &[serde_json::Value]) -> Res
5545
}
5646

5747
/// ABI-encode function return values (no selector prefix).
58-
fn encode_return_value(function_sig: &str, returns: &[serde_json::Value]) -> Result<Vec<u8>> {
48+
pub(super) fn encode_return_value(
49+
function_sig: &str,
50+
returns: &[serde_json::Value],
51+
) -> Result<Vec<u8>> {
5952
let alloy_sig = to_alloy_signature(function_sig);
6053
let function = Function::parse(&alloy_sig).map_err(|e| {
6154
anyhow!(
@@ -92,7 +85,7 @@ fn encode_return_value(function_sig: &str, returns: &[serde_json::Value]) -> Res
9285

9386
/// Convert graph-node `name(inputs):(outputs)` to alloy `name(inputs) returns (outputs)`.
9487
/// Passes through signatures already in alloy format or without outputs.
95-
fn to_alloy_signature(sig: &str) -> String {
88+
pub(super) fn to_alloy_signature(sig: &str) -> String {
9689
// If it already contains "returns", assume alloy format.
9790
if sig.contains(" returns ") {
9891
return sig.to_string();
@@ -108,49 +101,6 @@ fn to_alloy_signature(sig: &str) -> String {
108101
}
109102
}
110103

111-
/// Populate the eth_call cache from test block mock calls before indexing starts.
112-
pub async fn populate_eth_call_cache(
113-
logger: &Logger,
114-
chain_store: Arc<ChainStore>,
115-
blocks: &[BlockWithTriggers<Chain>],
116-
test_file: &TestFile,
117-
) -> Result<()> {
118-
for (block_data, test_block) in blocks.iter().zip(&test_file.blocks) {
119-
let block_ptr = block_data.ptr();
120-
121-
for eth_call in &test_block.eth_calls {
122-
populate_single_call(logger, chain_store.clone(), &block_ptr, eth_call).await?;
123-
}
124-
}
125-
Ok(())
126-
}
127-
128-
async fn populate_single_call(
129-
logger: &Logger,
130-
chain_store: Arc<ChainStore>,
131-
block_ptr: &BlockPtr,
132-
eth_call: &MockEthCall,
133-
) -> Result<()> {
134-
let address: Address = eth_call.address.parse()?;
135-
136-
let encoded_call = encode_function_call(&eth_call.function, &eth_call.params)?;
137-
138-
let request = call::Request::new(address, encoded_call, 0);
139-
140-
let retval = if eth_call.reverts {
141-
call::Retval::Null
142-
} else {
143-
let encoded_return = encode_return_value(&eth_call.function, &eth_call.returns)?;
144-
call::Retval::Value(encoded_return.into())
145-
};
146-
147-
chain_store
148-
.set_call(logger, request, block_ptr.clone(), retval)
149-
.await?;
150-
151-
Ok(())
152-
}
153-
154104
#[cfg(test)]
155105
mod tests {
156106
use super::*;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! Wraps Ethereum host functions to convert `PossibleReorg` errors into
2+
//! `Deterministic` ones so they surface immediately instead of causing
3+
//! infinite restart loops.
4+
5+
use anyhow::anyhow;
6+
use graph::blockchain::{self, ChainIdentifier, HostFn, HostFnCtx};
7+
use graph::data_source;
8+
use graph::futures03::FutureExt;
9+
use graph::prelude::EthereumCallCache;
10+
use graph::runtime::HostExportError;
11+
use graph_chain_ethereum::chain::{EthereumRuntimeAdapterBuilder, RuntimeAdapterBuilder};
12+
use graph_chain_ethereum::network::EthereumNetworkAdapters;
13+
use graph_chain_ethereum::Chain;
14+
use std::sync::Arc;
15+
16+
const WRAPPED_HOST_FNS: &[&str] = &["ethereum.call", "ethereum.getBalance", "ethereum.hasCode"];
17+
18+
pub struct TestRuntimeAdapterBuilder;
19+
20+
impl RuntimeAdapterBuilder for TestRuntimeAdapterBuilder {
21+
fn build(
22+
&self,
23+
eth_adapters: Arc<EthereumNetworkAdapters>,
24+
call_cache: Arc<dyn EthereumCallCache>,
25+
chain_identifier: Arc<ChainIdentifier>,
26+
) -> Arc<dyn blockchain::RuntimeAdapter<Chain>> {
27+
let real_adapter =
28+
EthereumRuntimeAdapterBuilder {}.build(eth_adapters, call_cache, chain_identifier);
29+
Arc::new(TestRuntimeAdapter { real_adapter })
30+
}
31+
}
32+
33+
struct TestRuntimeAdapter {
34+
real_adapter: Arc<dyn blockchain::RuntimeAdapter<Chain>>,
35+
}
36+
37+
impl TestRuntimeAdapter {
38+
/// Convert `PossibleReorg` → `Deterministic` for a single host function.
39+
fn wrap_possible_reorg(real: HostFn) -> HostFn {
40+
let real_func = real.func.clone();
41+
let name = real.name;
42+
HostFn {
43+
name,
44+
func: Arc::new(move |ctx: HostFnCtx<'_>, wasm_ptr: u32| {
45+
let real_func = real_func.clone();
46+
async move {
47+
match real_func(ctx, wasm_ptr).await {
48+
Ok(result) => Ok(result),
49+
Err(HostExportError::PossibleReorg(e)) => {
50+
Err(HostExportError::Deterministic(anyhow!(
51+
"{}. Add mock data to your test JSON.",
52+
e
53+
)))
54+
}
55+
Err(other) => Err(other),
56+
}
57+
}
58+
.boxed()
59+
}),
60+
}
61+
}
62+
}
63+
64+
impl blockchain::RuntimeAdapter<Chain> for TestRuntimeAdapter {
65+
fn host_fns(&self, ds: &data_source::DataSource<Chain>) -> Result<Vec<HostFn>, anyhow::Error> {
66+
let mut fns = self.real_adapter.host_fns(ds)?;
67+
68+
// PossibleReorg → Deterministic for mock-backed host functions.
69+
for hf in &mut fns {
70+
if WRAPPED_HOST_FNS.contains(&hf.name) {
71+
*hf = Self::wrap_possible_reorg(hf.clone());
72+
}
73+
}
74+
75+
Ok(fns)
76+
}
77+
}

0 commit comments

Comments
 (0)