Skip to content

Commit

Permalink
graph: Support declared calls for subgraph datasources
Browse files Browse the repository at this point in the history
  • Loading branch information
incrypto32 committed Dec 2, 2024
1 parent 1dd59c3 commit 94c19c2
Show file tree
Hide file tree
Showing 5 changed files with 694 additions and 237 deletions.
286 changes: 56 additions & 230 deletions chain/ethereum/src/data_source.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
use anyhow::{anyhow, Error};
use anyhow::{ensure, Context};
use graph::blockchain::block_stream::EntityWithType;
use graph::blockchain::{BlockPtr, TriggerWithHandler};
use graph::components::metrics::subgraph::SubgraphInstanceMetrics;
use graph::components::store::{EthereumCallCache, StoredDynamicDataSource};
use graph::components::subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError};
use graph::components::trigger_processor::RunnableTriggers;
use graph::data::value::Word;
use graph::data_source::common::{MappingABI, UnresolvedMappingABI};
use graph::data_source::common::{CallDecls, MappingABI, UnresolvedMappingABI};
use graph::data_source::subgraph::{EntityHandler, Mapping as SubgraphDataSourceMapping};
use graph::data_source::CausalityRegion;
use graph::env::ENV_VARS;
use graph::futures03::future::try_join;
use graph::futures03::stream::FuturesOrdered;
use graph::futures03::TryStreamExt;
use graph::prelude::ethabi::ethereum_types::H160;
use graph::prelude::ethabi::{StateMutability, Token};
use graph::prelude::lazy_static;
use graph::prelude::regex::Regex;
use graph::prelude::{Link, SubgraphManifestValidationError};
use graph::slog::{debug, error, o, trace};
use itertools::Itertools;
use serde::de;
use serde::de::Error as ErrorD;
use serde::{Deserialize, Deserializer};
use std::collections::HashSet;
Expand All @@ -31,7 +29,6 @@ use tiny_keccak::{keccak256, Keccak};

use graph::{
blockchain::{self, Blockchain},
derive::CheapClone,
prelude::{
async_trait,
ethabi::{Address, Event, Function, LogParam, ParamType, RawLog},
Expand Down Expand Up @@ -970,8 +967,59 @@ impl DeclaredCall {
})?
};

let address = decl.address(log, params)?;
let args = decl.args(log, params)?;
let address = decl.address_for_log(log, params)?;
let args = decl.args_for_log(log, params)?;

let call = DeclaredCall {
label: decl.label.clone(),
contract_name,
address,
function: function.clone(),
args,
};
calls.push(call);
}

Ok(calls)
}

fn from_entity_handler(
mapping: &SubgraphDataSourceMapping,
handler: &EntityHandler,
entity: &EntityWithType,
) -> Result<Vec<DeclaredCall>, anyhow::Error> {
let mut calls = Vec::new();
for decl in handler.calls.decls.iter() {
let contract_name = decl.expr.abi.to_string();
let function_name = decl.expr.func.as_str();
// Obtain the path to the contract ABI
let abi = mapping.find_abi(&contract_name)?;
// TODO: Handle overloaded functions
let function = {
// Behavior for apiVersion < 0.0.4: look up function by name; for overloaded
// functions this always picks the same overloaded variant, which is incorrect
// and may lead to encoding/decoding errors
abi.contract.function(function_name).with_context(|| {
format!(
"Unknown function \"{}::{}\" called from WASM runtime",
contract_name, function_name
)
})?
};

let param_types = function
.inputs
.iter()
.map(|param| param.kind.clone())
.collect::<Vec<_>>();

let address = decl.address_for_entity_handler(entity)?;
let args = decl
.args_for_entity_handler(entity, param_types)
.context(format!(
"Failed to parse arguments for call to function \"{}\" of contract \"{}\"",
function_name, contract_name
))?;

let call = DeclaredCall {
label: decl.label.clone(),
Expand Down Expand Up @@ -1569,225 +1617,3 @@ fn string_to_h256(s: &str) -> H256 {
pub struct TemplateSource {
pub abi: String,
}

/// Internal representation of declared calls. In the manifest that's
/// written as part of an event handler as
/// ```yaml
/// calls:
/// - myCall1: Contract[address].function(arg1, arg2, ...)
/// - ..
/// ```
///
/// The `address` and `arg` fields can be either `event.address` or
/// `event.params.<name>`. Each entry under `calls` gets turned into a
/// `CallDcl`
#[derive(Clone, CheapClone, Debug, Default, Hash, Eq, PartialEq)]
pub struct CallDecls {
pub decls: Arc<Vec<CallDecl>>,
readonly: (),
}

/// A single call declaration, like `myCall1:
/// Contract[address].function(arg1, arg2, ...)`
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct CallDecl {
/// A user-defined label
pub label: String,
/// The call expression
pub expr: CallExpr,
readonly: (),
}
impl CallDecl {
fn address(&self, log: &Log, params: &[LogParam]) -> Result<H160, Error> {
let address = match &self.expr.address {
CallArg::Address => log.address,
CallArg::HexAddress(address) => *address,
CallArg::Param(name) => {
let value = params
.iter()
.find(|param| &param.name == name.as_str())
.ok_or_else(|| anyhow!("unknown param {name}"))?
.value
.clone();
value
.into_address()
.ok_or_else(|| anyhow!("param {name} is not an address"))?
}
};
Ok(address)
}

fn args(&self, log: &Log, params: &[LogParam]) -> Result<Vec<Token>, Error> {
self.expr
.args
.iter()
.map(|arg| match arg {
CallArg::Address => Ok(Token::Address(log.address)),
CallArg::HexAddress(address) => Ok(Token::Address(*address)),
CallArg::Param(name) => {
let value = params
.iter()
.find(|param| &param.name == name.as_str())
.ok_or_else(|| anyhow!("unknown param {name}"))?
.value
.clone();
Ok(value)
}
})
.collect()
}
}

impl<'de> de::Deserialize<'de> for CallDecls {
fn deserialize<D>(deserializer: D) -> Result<CallDecls, D::Error>
where
D: de::Deserializer<'de>,
{
let decls: std::collections::HashMap<String, String> =
de::Deserialize::deserialize(deserializer)?;
let decls = decls
.into_iter()
.map(|(name, expr)| {
expr.parse::<CallExpr>().map(|expr| CallDecl {
label: name,
expr,
readonly: (),
})
})
.collect::<Result<_, _>>()
.map(|decls| Arc::new(decls))
.map_err(de::Error::custom)?;
Ok(CallDecls {
decls,
readonly: (),
})
}
}

#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct CallExpr {
pub abi: Word,
pub address: CallArg,
pub func: Word,
pub args: Vec<CallArg>,
readonly: (),
}

/// Parse expressions of the form `Contract[address].function(arg1, arg2,
/// ...)` where the `address` and the args are either `event.address` or
/// `event.params.<name>`.
///
/// The parser is pretty awful as it generates error messages that aren't
/// very helpful. We should replace all this with a real parser, most likely
/// `combine` which is what `graphql_parser` uses
impl FromStr for CallExpr {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static! {
static ref RE: Regex = Regex::new(
r"(?x)
(?P<abi>[a-zA-Z0-9_]+)\[
(?P<address>[^]]+)\]
\.
(?P<func>[a-zA-Z0-9_]+)\(
(?P<args>[^)]*)
\)"
)
.unwrap();
}
let x = RE
.captures(s)
.ok_or_else(|| anyhow!("invalid call expression `{s}`"))?;
let abi = Word::from(x.name("abi").unwrap().as_str());
let address = x.name("address").unwrap().as_str().parse()?;
let func = Word::from(x.name("func").unwrap().as_str());
let args: Vec<CallArg> = x
.name("args")
.unwrap()
.as_str()
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.trim().parse::<CallArg>())
.collect::<Result<_, _>>()?;
Ok(CallExpr {
abi,
address,
func,
args,
readonly: (),
})
}
}

#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum CallArg {
HexAddress(Address),
Address,
Param(Word),
}

lazy_static! {
// Matches a 40-character hexadecimal string prefixed with '0x', typical for Ethereum addresses
static ref ADDR_RE: Regex = Regex::new(r"^0x[0-9a-fA-F]{40}$").unwrap();
}

impl FromStr for CallArg {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if ADDR_RE.is_match(s) {
if let Ok(parsed_address) = Address::from_str(s) {
return Ok(CallArg::HexAddress(parsed_address));
}
}

let mut parts = s.split('.');
match (parts.next(), parts.next(), parts.next()) {
(Some("event"), Some("address"), None) => Ok(CallArg::Address),
(Some("event"), Some("params"), Some(param)) => Ok(CallArg::Param(Word::from(param))),
_ => Err(anyhow!("invalid call argument `{}`", s)),
}
}
}

#[test]
fn test_call_expr() {
let expr: CallExpr = "ERC20[event.address].balanceOf(event.params.token)"
.parse()
.unwrap();
assert_eq!(expr.abi, "ERC20");
assert_eq!(expr.address, CallArg::Address);
assert_eq!(expr.func, "balanceOf");
assert_eq!(expr.args, vec![CallArg::Param("token".into())]);

let expr: CallExpr = "Pool[event.params.pool].fees(event.params.token0, event.params.token1)"
.parse()
.unwrap();
assert_eq!(expr.abi, "Pool");
assert_eq!(expr.address, CallArg::Param("pool".into()));
assert_eq!(expr.func, "fees");
assert_eq!(
expr.args,
vec![
CallArg::Param("token0".into()),
CallArg::Param("token1".into())
]
);

let expr: CallExpr = "Pool[event.address].growth()".parse().unwrap();
assert_eq!(expr.abi, "Pool");
assert_eq!(expr.address, CallArg::Address);
assert_eq!(expr.func, "growth");
assert_eq!(expr.args, vec![]);

let expr: CallExpr = "Pool[0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF].growth(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF)"
.parse()
.unwrap();
let call_arg =
CallArg::HexAddress(H160::from_str("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF").unwrap());
assert_eq!(expr.abi, "Pool");
assert_eq!(expr.address, call_arg);
assert_eq!(expr.func, "growth");
assert_eq!(expr.args, vec![call_arg]);
}
1 change: 1 addition & 0 deletions graph/src/data/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,7 @@ where
}
}


lazy_static! {
/// The name of the id attribute, `"id"`
pub static ref ID: Word = Word::from("id");
Expand Down
Loading

0 comments on commit 94c19c2

Please sign in to comment.