Skip to content

Commit e872671

Browse files
committed
Wire up Rust ABI dispatch, trigger serialization, and live-test fixes
- Add Ethereum ToRustBytes impl for Log/Call/Block triggers - Add NEAR ToRustBytes stub (unimplemented, Ethereum-only for now) - Propagate ToRustBytes trait bounds through instance_manager - Skip parity_wasm gas injection for Rust modules (can't parse bulk-memory opcodes) - Skip AS-specific exports (id_of_type, _start) for Rust modules - Add handle_trigger_rust and invoke_handler_rust calling convention - Add Rust host function wrappers in module/context.rs
1 parent 113bdd2 commit e872671

15 files changed

Lines changed: 643 additions & 69 deletions

File tree

chain/ethereum/src/ethereum_adapter.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,18 @@ impl EthereumAdapter {
698698
.map_err(|e| e.into_inner().unwrap_or(ContractCallError::Timeout))
699699
}
700700

701+
/// Make a raw eth_call without ABI encoding.
702+
/// Used by Rust ABI subgraphs where the SDK handles encoding/decoding.
703+
pub async fn raw_call(
704+
&self,
705+
req: call::Request,
706+
block_ptr: BlockPtr,
707+
gas: Option<u32>,
708+
) -> Result<call::Retval, ContractCallError> {
709+
let logger = self.provider_logger(&self.logger);
710+
self.call(logger, req, block_ptr, gas).await
711+
}
712+
701713
async fn call_and_cache(
702714
&self,
703715
logger: &ProviderLogger,

chain/ethereum/src/runtime/runtime_adapter.rs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::{sync::Arc, time::Instant};
22

3+
use async_trait::async_trait;
4+
35
use crate::adapter::EthereumRpcError;
46
use crate::{
57
capabilities::NodeCapabilities, network::EthereumNetworkAdapters, Chain, ContractCallError,
@@ -9,7 +11,7 @@ use anyhow::{anyhow, Context, Error};
911
use blockchain::HostFn;
1012
use graph::abi;
1113
use graph::abi::DynSolValueExt;
12-
use graph::blockchain::ChainIdentifier;
14+
use graph::blockchain::{ChainIdentifier, RawEthCall};
1315
use graph::components::subgraph::HostMetrics;
1416
use graph::data::store::ethereum::call;
1517
use graph::data::store::scalar::BigInt;
@@ -18,7 +20,7 @@ use graph::data_source;
1820
use graph::data_source::common::{ContractCall, MappingABI};
1921
use graph::runtime::gas::Gas;
2022
use graph::runtime::{AscIndexId, IndexForAscTypeId};
21-
use graph::slog::debug;
23+
use graph::slog::{debug, o, Discard};
2224
use graph::{
2325
blockchain::{self, BlockPtr, HostFnCtx},
2426
cheap_clone::CheapClone,
@@ -185,6 +187,101 @@ impl blockchain::RuntimeAdapter<Chain> for RuntimeAdapter {
185187

186188
Ok(host_fns)
187189
}
190+
191+
fn raw_eth_call(&self) -> Option<Arc<dyn RawEthCall>> {
192+
Some(Arc::new(EthereumRawEthCall {
193+
eth_adapters: self.eth_adapters.cheap_clone(),
194+
call_cache: self.call_cache.cheap_clone(),
195+
eth_call_gas: eth_call_gas(&self.chain_identifier),
196+
}))
197+
}
198+
}
199+
200+
/// Implementation of RawEthCall for Ethereum chains.
201+
/// Used by Rust ABI subgraphs for making raw eth_call without ABI encoding.
202+
pub struct EthereumRawEthCall {
203+
eth_adapters: Arc<EthereumNetworkAdapters>,
204+
call_cache: Arc<dyn EthereumCallCache>,
205+
eth_call_gas: Option<u32>,
206+
}
207+
208+
#[async_trait]
209+
impl RawEthCall for EthereumRawEthCall {
210+
async fn call(
211+
&self,
212+
address: [u8; 20],
213+
calldata: &[u8],
214+
block_ptr: &BlockPtr,
215+
gas: Option<u32>,
216+
) -> Result<Option<Vec<u8>>, HostExportError> {
217+
// Get an adapter suitable for calls (non-archive is fine)
218+
let eth_adapter = self
219+
.eth_adapters
220+
.call_or_cheapest(Some(&NodeCapabilities {
221+
archive: false,
222+
traces: false,
223+
}))
224+
.map_err(|e| HostExportError::Unknown(e.into()))?;
225+
226+
// Create a raw call request
227+
let req = call::Request::new(Address::from(address), calldata.to_vec(), 0);
228+
229+
// Check cache first
230+
let (cached, _missing) = self
231+
.call_cache
232+
.get_calls(&[req.cheap_clone()], block_ptr.cheap_clone())
233+
.await
234+
.unwrap_or_else(|_| (Vec::new(), vec![req.cheap_clone()]));
235+
236+
if let Some(resp) = cached.into_iter().next() {
237+
return match resp.retval {
238+
call::Retval::Value(bytes) => Ok(Some(bytes.to_vec())),
239+
call::Retval::Null => Ok(None),
240+
};
241+
}
242+
243+
// Make the actual call
244+
let result = eth_adapter
245+
.raw_call(
246+
req.cheap_clone(),
247+
block_ptr.cheap_clone(),
248+
gas.or(self.eth_call_gas),
249+
)
250+
.await;
251+
252+
match result {
253+
Ok(retval) => {
254+
// Cache the result
255+
let cache = self.call_cache.cheap_clone();
256+
let _ = cache
257+
.set_call(
258+
&Logger::root(Discard, o!()),
259+
req,
260+
block_ptr.cheap_clone(),
261+
retval.clone(),
262+
)
263+
.await;
264+
265+
match retval {
266+
call::Retval::Value(bytes) => Ok(Some(bytes.to_vec())),
267+
call::Retval::Null => Ok(None),
268+
}
269+
}
270+
Err(ContractCallError::AlloyError(e)) => {
271+
Err(HostExportError::PossibleReorg(anyhow::anyhow!(
272+
"eth_call RPC error: {}",
273+
e
274+
)))
275+
}
276+
Err(ContractCallError::Timeout) => Err(HostExportError::PossibleReorg(anyhow::anyhow!(
277+
"eth_call timed out"
278+
))),
279+
Err(e) => Err(HostExportError::Unknown(anyhow::anyhow!(
280+
"eth_call failed: {}",
281+
e
282+
))),
283+
}
284+
}
188285
}
189286

190287
/// function ethereum.call(call: SmartContractCall): Array<Token> | null

chain/ethereum/src/trigger.rs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use graph::runtime::AscPtr;
2323
use graph::runtime::HostExportError;
2424
use graph::semver::Version;
2525
use graph_runtime_wasm::module::ToAscPtr;
26-
use graph_runtime_wasm::rust_abi::{RustLogTrigger, ToRustBytes};
26+
use graph_runtime_wasm::rust_abi::{RustBlockTrigger, RustCallTrigger, RustLogTrigger, ToRustBytes};
2727
use std::{cmp::Ordering, sync::Arc};
2828

2929
use crate::runtime::abi::AscEthereumBlock;
@@ -666,7 +666,6 @@ impl ToRustBytes for MappingTrigger {
666666
receipt: _,
667667
calls: _,
668668
} => {
669-
// Convert to RustLogTrigger for serialization
670669
let rust_trigger = RustLogTrigger {
671670
address: log.inner.address.0 .0,
672671
tx_hash: transaction.tx_hash().0,
@@ -684,13 +683,41 @@ impl ToRustBytes for MappingTrigger {
684683
};
685684
rust_trigger.to_rust_bytes()
686685
}
687-
MappingTrigger::Call { .. } => {
688-
// TODO: Implement call trigger serialization
689-
Vec::new()
686+
MappingTrigger::Call {
687+
block,
688+
transaction,
689+
call,
690+
inputs: _,
691+
outputs: _,
692+
} => {
693+
let rust_trigger = RustCallTrigger {
694+
to: call.to.0 .0,
695+
from: call.from.0 .0,
696+
tx_hash: transaction.tx_hash().0,
697+
block_number: block.number_u64(),
698+
block_timestamp: block.inner().header.timestamp,
699+
block_hash: block.inner().header.hash.0,
700+
input: call.input.to_vec(),
701+
output: call.output.to_vec(),
702+
};
703+
rust_trigger.to_rust_bytes()
690704
}
691-
MappingTrigger::Block { .. } => {
692-
// TODO: Implement block trigger serialization
693-
Vec::new()
705+
MappingTrigger::Block { block } => {
706+
// Convert U256 difficulty to big-endian bytes
707+
let difficulty: [u8; 32] = block.inner().header.difficulty.to_be_bytes();
708+
709+
let rust_trigger = RustBlockTrigger {
710+
hash: block.inner().header.hash.0,
711+
parent_hash: block.inner().header.parent_hash.0,
712+
number: block.number_u64(),
713+
timestamp: block.inner().header.timestamp,
714+
author: block.inner().header.beneficiary.0 .0,
715+
gas_used: block.inner().header.gas_used,
716+
gas_limit: block.inner().header.gas_limit,
717+
difficulty,
718+
base_fee_per_gas: block.inner().header.base_fee_per_gas.unwrap_or(0),
719+
};
720+
rust_trigger.to_rust_bytes()
694721
}
695722
}
696723
}

chain/near/src/trigger.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use graph::prelude::BlockNumber;
99
use graph::runtime::HostExportError;
1010
use graph::runtime::{asc_new, gas::GasCounter, AscHeap, AscPtr};
1111
use graph_runtime_wasm::module::ToAscPtr;
12+
use graph_runtime_wasm::rust_abi::ToRustBytes;
1213
use std::{cmp::Ordering, sync::Arc};
1314

1415
use crate::codec;
@@ -143,6 +144,14 @@ impl MappingTriggerTrait for NearTrigger {
143144
}
144145
}
145146

147+
impl ToRustBytes for NearTrigger {
148+
fn to_rust_bytes(&self) -> Vec<u8> {
149+
// NEAR triggers are not yet supported by Graphite SDK.
150+
// This stub satisfies the trait bound so Ethereum Rust subgraphs can compile.
151+
unimplemented!("Rust ABI serialization is not yet supported for NEAR triggers")
152+
}
153+
}
154+
146155
pub struct ReceiptWithOutcome {
147156
// REVIEW: Do we want to actually also have those two below behind an `Arc` wrapper?
148157
pub outcome: codec::ExecutionOutcomeWithId,

core/src/subgraph/instance_manager.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use graph::env::EnvVars;
2424
use graph::prelude::{SubgraphInstanceManager as SubgraphInstanceManagerTrait, *};
2525
use graph::{blockchain::BlockchainMap, components::store::DeploymentLocator};
2626
use graph_runtime_wasm::module::ToAscPtr;
27+
use graph_runtime_wasm::rust_abi::ToRustBytes;
2728
use graph_runtime_wasm::RuntimeHostBuilder;
2829
use tokio::task;
2930

@@ -234,7 +235,7 @@ impl<S: SubgraphStore, AC: amp::Client> SubgraphInstanceManager<S, AC> {
234235
) -> anyhow::Result<SubgraphRunner<C, RuntimeHostBuilder<C>>>
235236
where
236237
C: Blockchain,
237-
<C as Blockchain>::MappingTrigger: ToAscPtr,
238+
<C as Blockchain>::MappingTrigger: ToAscPtr + ToRustBytes,
238239
{
239240
self.build_subgraph_runner_inner(
240241
logger,
@@ -262,7 +263,7 @@ impl<S: SubgraphStore, AC: amp::Client> SubgraphInstanceManager<S, AC> {
262263
) -> anyhow::Result<SubgraphRunner<C, RuntimeHostBuilder<C>>>
263264
where
264265
C: Blockchain,
265-
<C as Blockchain>::MappingTrigger: ToAscPtr,
266+
<C as Blockchain>::MappingTrigger: ToAscPtr + ToRustBytes,
266267
{
267268
let subgraph_store = self.subgraph_store.cheap_clone();
268269
let registry = self.metrics_registry.cheap_clone();
@@ -560,7 +561,7 @@ impl<S: SubgraphStore, AC: amp::Client> SubgraphInstanceManager<S, AC> {
560561
runner: SubgraphRunner<C, RuntimeHostBuilder<C>>,
561562
) -> Result<(), Error>
562563
where
563-
<C as Blockchain>::MappingTrigger: ToAscPtr,
564+
<C as Blockchain>::MappingTrigger: ToAscPtr + ToRustBytes,
564565
{
565566
let registry = self.metrics_registry.cheap_clone();
566567
let subgraph_metrics = runner.metrics.subgraph.cheap_clone();

graph/src/blockchain/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,27 @@ pub struct HostFn {
546546
#[async_trait]
547547
pub trait RuntimeAdapter<C: Blockchain>: Send + Sync {
548548
fn host_fns(&self, ds: &data_source::DataSource<C>) -> Result<Vec<HostFn>, Error>;
549+
550+
/// Get a raw eth_call capability for Rust ABI subgraphs.
551+
/// Returns None if the chain doesn't support raw eth_call (e.g., non-EVM chains).
552+
fn raw_eth_call(&self) -> Option<Arc<dyn RawEthCall>> {
553+
None
554+
}
555+
}
556+
557+
/// Trait for making raw eth_call requests without ABI encoding.
558+
/// Used by Rust ABI subgraphs where the SDK handles encoding/decoding.
559+
#[async_trait]
560+
pub trait RawEthCall: Send + Sync {
561+
/// Make a raw eth_call to the given address with the provided calldata.
562+
/// Returns Ok(Some(bytes)) on success, Ok(None) on revert, Err on RPC error.
563+
async fn call(
564+
&self,
565+
address: [u8; 20],
566+
calldata: &[u8],
567+
block_ptr: &BlockPtr,
568+
gas: Option<u32>,
569+
) -> Result<Option<Vec<u8>>, HostExportError>;
549570
}
550571

551572
pub trait NodeCapabilities<C: Blockchain> {

runtime/test/src/common.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ pub fn mock_context(
130130
),
131131
proof_of_indexing: SharedProofOfIndexing::ignored(),
132132
host_fns: Arc::new(Vec::new()),
133+
raw_eth_call: None,
133134
debug_fork: None,
134135
mapping_logger: Logger::root(slog::Discard, o!()),
135136
instrument: false,

runtime/wasm/src/host.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use async_trait::async_trait;
55
use graph::futures01::sync::mpsc::Sender;
66
use graph::futures03::channel::oneshot::channel;
77

8-
use graph::blockchain::{Blockchain, HostFn, RuntimeAdapter};
8+
use graph::blockchain::{Blockchain, HostFn, RawEthCall, RuntimeAdapter};
99
use graph::components::store::{EnsLookup, SubgraphFork};
1010
use graph::components::subgraph::{MappingError, SharedProofOfIndexing};
1111
use graph::data_source::{
@@ -106,6 +106,7 @@ where
106106

107107
pub struct RuntimeHost<C: Blockchain> {
108108
host_fns: Arc<Vec<HostFn>>,
109+
raw_eth_call: Option<Arc<dyn RawEthCall>>,
109110
data_source: DataSource<C>,
110111
mapping_request_sender: Sender<WasmRequest<C>>,
111112
host_exports: Arc<HostExports>,
@@ -143,9 +144,11 @@ where
143144
));
144145

145146
let host_fns = runtime_adapter.host_fns(&data_source).unwrap_or_default();
147+
let raw_eth_call = runtime_adapter.raw_eth_call();
146148

147149
Ok(RuntimeHost {
148150
host_fns: Arc::new(host_fns),
151+
raw_eth_call,
149152
data_source,
150153
mapping_request_sender,
151154
host_exports,
@@ -189,6 +192,7 @@ where
189192
timestamp: trigger.timestamp(),
190193
proof_of_indexing,
191194
host_fns: self.host_fns.cheap_clone(),
195+
raw_eth_call: self.raw_eth_call.cheap_clone(),
192196
debug_fork: debug_fork.cheap_clone(),
193197
mapping_logger: Logger::new(logger, o!("component" => "UserMapping")),
194198
instrument,

0 commit comments

Comments
 (0)