From d52216c3f1756706d37ea02c1b074601d3982cf4 Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Thu, 8 Aug 2024 01:37:14 +0100 Subject: [PATCH 1/2] Add support for fetching events Closes #3 --- CHANGELOG.md | 1 + docs/src/builtin_values.md | 7 ++ docs/src/interacting_with_contracts.md | 30 +++++- src/interpreter/builtins/events.rs | 136 +++++++++++++++++++++++++ src/interpreter/builtins/mod.rs | 6 ++ src/interpreter/types.rs | 17 ++++ src/interpreter/value.rs | 32 +++--- 7 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 src/interpreter/builtins/events.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b164b37..94fb0bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Add `array.filter` function * Add support for bitwise operators * [EXPERIMENTAL] Add support for anonymous functions +* [EXPERIMENTAL] Add support for fetching events ### Bug fixes diff --git a/docs/src/builtin_values.md b/docs/src/builtin_values.md index b0f113c..87634b1 100644 --- a/docs/src/builtin_values.md +++ b/docs/src/builtin_values.md @@ -234,3 +234,10 @@ Returns the current block base fee. ### `block.chainid -> uint256` Returns the current chain ID. + +## `events` functions + +### `events.fetch{options}(address target) -> Log[] | events.fetch{options}(address[] targets) -> Log[]` + +Fetches the events emitted by the contract(s) at the given address(es). +For more information, see [events](./interacting_with_contracts.md#events). diff --git a/docs/src/interacting_with_contracts.md b/docs/src/interacting_with_contracts.md index abf3e83..13786a8 100644 --- a/docs/src/interacting_with_contracts.md +++ b/docs/src/interacting_with_contracts.md @@ -59,4 +59,32 @@ TransactionReceipt { tx_hash: 0x248ad948d1e4eefc6ccb271cac2001ebbdb2346beddc7656 [Log { address: 0x6B175474E89094C44Da98b954EedeAC495271d0F, topics: [0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266, 0x00000000000000000000000083f20f44975d03b1b09e64809b757c47f942beea], data: 0x0000000000000000000000000000000000000000000000000de0b6b3a7640000 }] ``` -Note: Event decoding is not implemented yet +If the ABI of the contract emitting the log is loaded, the logs will automatically be decoded and the decoded arguments will be available in the `args` property of each log. + +## Events + +Eclair provides a way to fetch events emitted by a contract using the `events.fetch` method. + +```javascript +>> events.fetch{fromBlock: 20490506, toBlock: 20490512}(0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A)[0] +Log { address: 0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A, topics: [0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0x000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8, 0x000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd67], data: 0x00000000000000000000000000000000000000000000000e8bd6d724bc4c7886, args: Transfer { from: 0xBA12222222228d8Ba445958a75a0704d566BF2C8, to: 0xf081470f5C6FBCCF48cC4e5B82Dd926409DcdD67, value: 268330894800999708806 } } +``` + +The `events.fetch` accepts either a single address or a list of addresses as the first argument, as well as some options +to filter the logs returned. +It returns a list of logs that match the given criteria, and automatically decodes each log if the ABI is loaded. + +### Options + +The `events.fetch` method accepts the following options: + +* `fromBlock`: the block number to start fetching events from +* `toBlock`: the block number to stop fetching events at +* `topic0`: topic0 of the event +* `topic1`: topic1 of the event +* `topic2`: topic2 of the event +* `topic3`: topic3 of the event + +By default, it will try to fetch from the first ever block to the latest block. +In many cases, the RPC provider will reject the request because too much data would be returned, in which case +options above will need to be added to restrict the size of the response. diff --git a/src/interpreter/builtins/events.rs b/src/interpreter/builtins/events.rs new file mode 100644 index 0000000..5210e63 --- /dev/null +++ b/src/interpreter/builtins/events.rs @@ -0,0 +1,136 @@ +use std::sync::Arc; + +use alloy::{primitives::B256, rpc::types::Filter}; +use anyhow::{bail, Result}; +use futures::{future::BoxFuture, FutureExt}; +use lazy_static::lazy_static; + +use crate::interpreter::{functions::FunctionDef, types::LOG_TYPE, utils, Env, Type, Value}; + +#[derive(Debug)] +struct EventOptions { + topic0: Option, + topic1: Option, + topic2: Option, + topic3: Option, + from_block: Option, + to_block: Option, +} + +impl TryFrom<&crate::interpreter::types::HashableIndexMap> for EventOptions { + type Error = anyhow::Error; + + fn try_from(map: &crate::interpreter::types::HashableIndexMap) -> Result { + let topic0 = map.0.get("topic0").map(|v| v.as_b256()).transpose()?; + let topic1 = map.0.get("topic1").map(|v| v.as_b256()).transpose()?; + let topic2 = map.0.get("topic2").map(|v| v.as_b256()).transpose()?; + let topic3 = map.0.get("topic3").map(|v| v.as_b256()).transpose()?; + let from_block = map.0.get("fromBlock").map(|v| v.as_u64()).transpose()?; + let to_block = map.0.get("toBlock").map(|v| v.as_u64()).transpose()?; + + Ok(EventOptions { + topic0, + topic1, + topic2, + topic3, + from_block, + to_block, + }) + } +} + +fn fetch_events<'a>( + env: &'a mut Env, + args: &'a [Value], + options: EventOptions, +) -> BoxFuture<'a, Result> { + async move { + let mut filter = Filter::new(); + if let Some(topic0) = options.topic0 { + filter = filter.event_signature(topic0); + } + if let Some(topic1) = options.topic1 { + filter = filter.topic1(topic1); + } + if let Some(topic2) = options.topic2 { + filter = filter.topic2(topic2); + } + if let Some(topic3) = options.topic3 { + filter = filter.topic3(topic3); + } + if let Some(from_block) = options.from_block { + filter = filter.from_block(from_block); + } else { + filter = filter.from_block(0); + } + if let Some(to_block) = options.to_block { + filter = filter.to_block(to_block); + } + + match args { + [Value::Addr(addr)] => filter = filter.address(*addr), + [Value::Array(addrs, ty_)] if ty_.as_ref() == &Type::Address => { + let addresses = addrs + .iter() + .map(|a| a.as_address()) + .collect::>>()?; + filter = filter.address(addresses) + } + _ => bail!("events.fetch: invalid arguments"), + } + + let logs = env.get_provider().get_logs(&filter).await?; + let parsed_logs = logs + .into_iter() + .map(|log| utils::log_to_value(env, log)) + .collect::>>()?; + Ok(Value::Array(parsed_logs, Box::new(LOG_TYPE.clone()))) + } + .boxed() +} + +#[derive(Debug)] +struct FetchEvents; + +impl FunctionDef for FetchEvents { + fn name(&self) -> String { + "fetch".to_string() + } + + fn get_valid_args( + &self, + _receiver: &Option, + ) -> Vec> { + vec![ + vec![crate::interpreter::functions::FunctionParam::new( + "address", + Type::Address, + )], + vec![crate::interpreter::functions::FunctionParam::new( + "addresses", + Type::Array(Box::new(Type::Address)), + )], + ] + } + + fn is_property(&self) -> bool { + false + } + + fn execute<'a>( + &'a self, + env: &'a mut Env, + values: &'a [Value], + options: &'a crate::interpreter::types::HashableIndexMap, + ) -> BoxFuture<'a, Result> { + async move { + let parsed_opts = options.try_into()?; + fetch_events(env, &values[1..], parsed_opts).await + } + .boxed() + } +} + +lazy_static! { + pub static ref FETCH_EVENTS: Arc = Arc::new(FetchEvents); +} diff --git a/src/interpreter/builtins/mod.rs b/src/interpreter/builtins/mod.rs index 5e1b5e6..3714dc9 100644 --- a/src/interpreter/builtins/mod.rs +++ b/src/interpreter/builtins/mod.rs @@ -9,6 +9,7 @@ mod address; mod block; mod concat; mod console; +mod events; mod format; mod iterable; mod misc; @@ -29,6 +30,7 @@ lazy_static! { m.insert("repl".to_string(), Value::TypeObject(Type::Repl)); m.insert("console".to_string(), Value::TypeObject(Type::Console)); m.insert("block".to_string(), Value::TypeObject(Type::Block)); + m.insert("events".to_string(), Value::TypeObject(Type::Events)); m.insert( "Transaction".to_string(), Value::TypeObject(Type::Transaction), @@ -132,6 +134,10 @@ lazy_static! { console_methods.insert("log".to_string(), console::CONSOLE_LOG.clone()); m.insert(NonParametricType::Console, console_methods); + let mut event_methods = HashMap::new(); + event_methods.insert("fetch".to_string(), events::FETCH_EVENTS.clone()); + m.insert(NonParametricType::Events, event_methods); + let mut repl_methods = HashMap::new(); repl_methods.insert("vars".to_string(), repl::REPL_LIST_VARS.clone()); repl_methods.insert("types".to_string(), repl::REPL_LIST_TYPES.clone()); diff --git a/src/interpreter/types.rs b/src/interpreter/types.rs index 79193a3..a1074d9 100644 --- a/src/interpreter/types.rs +++ b/src/interpreter/types.rs @@ -1,3 +1,4 @@ +use lazy_static::lazy_static; use std::fmt::Display; use alloy::{ @@ -91,6 +92,7 @@ pub enum NonParametricType { Repl, Block, Console, + Events, Abi, Type, } @@ -118,6 +120,7 @@ pub enum Type { Repl, Block, Console, + Events, Abi, Type(Box), } @@ -153,6 +156,7 @@ impl Display for Type { Type::Repl => write!(f, "repl"), Type::Block => write!(f, "block"), + Type::Events => write!(f, "events"), Type::Console => write!(f, "console"), Type::Abi => write!(f, "abi"), Type::Type(t) => write!(f, "type({})", t), @@ -184,6 +188,7 @@ impl> From for NonParametricType { Type::Repl => NonParametricType::Repl, Type::Block => NonParametricType::Block, Type::Console => NonParametricType::Console, + Type::Events => NonParametricType::Events, Type::Abi => NonParametricType::Abi, Type::Type(_) => NonParametricType::Type, } @@ -537,6 +542,18 @@ impl Type { } } +lazy_static! { + pub static ref LOG_TYPE: Type = Type::NamedTuple( + "Log".to_string(), + HashableIndexMap::from_iter([ + ("address".to_string(), Type::Address), + ("topics".to_string(), Type::Array(Box::new(Type::Uint(256)))), + ("data".to_string(), Type::Bytes), + ("args".to_string(), Type::Any), + ]), + ); +} + #[cfg(test)] mod tests { use crate::interpreter::Value; diff --git a/src/interpreter/value.rs b/src/interpreter/value.rs index 6101746..bb5268c 100644 --- a/src/interpreter/value.rs +++ b/src/interpreter/value.rs @@ -17,7 +17,7 @@ use std::{ use super::{ builtins::{INSTANCE_METHODS, STATIC_METHODS, TYPE_METHODS}, functions::Function, - types::{ContractInfo, HashableIndexMap, Type}, + types::{ContractInfo, HashableIndexMap, Type, LOG_TYPE}, }; #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -386,7 +386,9 @@ impl Value { pub fn is_builtin(&self) -> bool { matches!( self, - Value::TypeObject(Type::Console) | Value::TypeObject(Type::Repl) + Value::TypeObject(Type::Console) + | Value::TypeObject(Type::Repl) + | Value::TypeObject(Type::Events) ) } @@ -421,7 +423,13 @@ impl Value { pub fn as_usize(&self) -> Result { match self { - Value::Int(n, _) => Ok(n.as_usize()), + Value::Int(n, _) => { + if n.is_positive() { + Ok(n.as_usize()) + } else { + bail!("negative number") + } + } Value::Uint(n, _) => Ok(n.to()), _ => bail!("cannot convert {} to usize", self.get_type()), } @@ -449,6 +457,13 @@ impl Value { } } + pub fn as_b256(&self) -> Result { + match self { + Value::FixBytes(n, 32) => Ok(*n), + _ => bail!("cannot convert {} to b256", self.get_type()), + } + } + pub fn as_block_id(&self) -> Result { match self { Value::FixBytes(hash, 32) => Ok(BlockId::Hash((*hash).into())), @@ -551,15 +566,6 @@ impl Value { } pub fn from_receipt(receipt: TransactionReceipt, parsed_logs: Vec) -> Self { - let log_type = Type::NamedTuple( - "Log".to_string(), - HashableIndexMap::from_iter([ - ("address".to_string(), Type::Address), - ("topics".to_string(), Type::Array(Box::new(Type::Uint(256)))), - ("data".to_string(), Type::Bytes), - ("args".to_string(), Type::Any), - ]), - ); let fields = IndexMap::from([ ( "txHash".to_string(), @@ -578,7 +584,7 @@ impl Value { ("gasPrice".to_string(), receipt.effective_gas_price.into()), ( "logs".to_string(), - Value::Array(parsed_logs, Box::new(log_type)), + Value::Array(parsed_logs, Box::new(LOG_TYPE.clone())), ), ]); From 301450c33f62d44612df8043d17556c355e9877a Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Fri, 9 Aug 2024 19:11:54 +0100 Subject: [PATCH 2/2] Add method to retrieve event selectors --- docs/src/builtin_methods.md | 6 ++++ docs/src/interacting_with_contracts.md | 7 +++++ src/interpreter/builtins/event.rs | 22 +++++++++++++++ src/interpreter/builtins/mod.rs | 9 ++++-- src/interpreter/types.rs | 38 ++++++++++++++++++++++---- src/interpreter/value.rs | 1 + 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 src/interpreter/builtins/event.rs diff --git a/docs/src/builtin_methods.md b/docs/src/builtin_methods.md index d7010be..a6050d4 100644 --- a/docs/src/builtin_methods.md +++ b/docs/src/builtin_methods.md @@ -158,6 +158,12 @@ The first element of the tuple is the function signature, and the second element ("transfer(address,uint256)", (0x789f8F7B547183Ab8E99A5e0E6D567E90e0EB03B, 100000000000000000000)) ``` +## `Event` static methods + +### `Event.selector -> bytes32` + +Returns the selector (aka topic0) of the given event + ## `num` (`uint*` and `int*`) static methods ### `type(num).max -> num` diff --git a/docs/src/interacting_with_contracts.md b/docs/src/interacting_with_contracts.md index 13786a8..a86d340 100644 --- a/docs/src/interacting_with_contracts.md +++ b/docs/src/interacting_with_contracts.md @@ -88,3 +88,10 @@ The `events.fetch` method accepts the following options: By default, it will try to fetch from the first ever block to the latest block. In many cases, the RPC provider will reject the request because too much data would be returned, in which case options above will need to be added to restrict the size of the response. + +To only get one type of event, e.g. `Transfer`, you can filter using `topic0` and the selector of the desired event. + +```javascript +>> events.fetch{fromBlock: 20490506, toBlock: 20490512, topic0: ERC20.Approval.selector}(0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A)[0] +Log { address: 0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A, topics: [0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, 0x0000000000000000000000008149dc18d39fdba137e43c871e7801e7cf566d41, 0x000000000000000000000000ea50f402653c41cadbafd1f788341db7b7f37816], data: 0x000000000000000000000000000000000000000000000025f273933db5700000, args: Approval { owner: 0x8149DC18D39FDBa137E43C871e7801E7CF566D41, spender: 0xeA50f402653c41cAdbaFD1f788341dB7B7F37816, value: 700000000000000000000 } } +``` diff --git a/src/interpreter/builtins/event.rs b/src/interpreter/builtins/event.rs new file mode 100644 index 0000000..648697a --- /dev/null +++ b/src/interpreter/builtins/event.rs @@ -0,0 +1,22 @@ +use std::sync::Arc; + +use crate::interpreter::{ + functions::{FunctionDef, SyncProperty}, + Env, Type, Value, +}; +use anyhow::{bail, Result}; +use lazy_static::lazy_static; + +pub fn event_selector(_env: &Env, receiver: &Value) -> Result { + let event_abi = match receiver { + Value::TypeObject(Type::Event(event)) => event, + _ => bail!("selector function expects receiver to be an event"), + }; + + Ok(event_abi.selector().into()) +} + +lazy_static! { + pub static ref EVENT_SELECTOR: Arc = + SyncProperty::arc("selector", event_selector); +} diff --git a/src/interpreter/builtins/mod.rs b/src/interpreter/builtins/mod.rs index 3714dc9..5fa4882 100644 --- a/src/interpreter/builtins/mod.rs +++ b/src/interpreter/builtins/mod.rs @@ -9,6 +9,7 @@ mod address; mod block; mod concat; mod console; +mod event; mod events; mod format; mod iterable; @@ -135,8 +136,12 @@ lazy_static! { m.insert(NonParametricType::Console, console_methods); let mut event_methods = HashMap::new(); - event_methods.insert("fetch".to_string(), events::FETCH_EVENTS.clone()); - m.insert(NonParametricType::Events, event_methods); + event_methods.insert("selector".to_string(), event::EVENT_SELECTOR.clone()); + m.insert(NonParametricType::Event, event_methods); + + let mut events_methods = HashMap::new(); + events_methods.insert("fetch".to_string(), events::FETCH_EVENTS.clone()); + m.insert(NonParametricType::Events, events_methods); let mut repl_methods = HashMap::new(); repl_methods.insert("vars".to_string(), repl::REPL_LIST_VARS.clone()); diff --git a/src/interpreter/types.rs b/src/interpreter/types.rs index a1074d9..41759b1 100644 --- a/src/interpreter/types.rs +++ b/src/interpreter/types.rs @@ -6,7 +6,7 @@ use alloy::{ json_abi::JsonAbi, primitives::{Address, B256, I256, U160, U256}, }; -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use indexmap::IndexMap; use itertools::Itertools; use solang_parser::pt as parser; @@ -61,12 +61,28 @@ impl ContractInfo { let _func = self .1 .function(name) - .ok_or_else(|| anyhow::anyhow!("function {} not found in contract {}", name, self.0))?; + .ok_or_else(|| anyhow!("function {} not found in contract {}", name, self.0))?; Ok(Function::new( ContractFunction::arc(name), Some(&Value::Contract(self.clone(), addr)), )) } + + pub fn member_access(&self, name: &str) -> Result { + if let Some(event) = self.1.events.get(name).and_then(|v| v.first()) { + return Ok(Value::TypeObject(Type::Event(event.clone()))); + } + let func = STATIC_METHODS + .get(&NonParametricType::Contract) + .unwrap() + .get(name) + .ok_or(anyhow!("{} not found in contract {}", name, self.0))?; + Ok(Function::method( + func.clone(), + &Value::TypeObject(Type::Contract(self.clone())), + ) + .into()) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -86,6 +102,7 @@ pub enum NonParametricType { Tuple, Mapping, Contract, + Event, Transaction, TransactionReceipt, Function, @@ -114,6 +131,7 @@ pub enum Type { Tuple(Vec), Mapping(Box, Box), Contract(ContractInfo), + Event(alloy::json_abi::Event), Transaction, TransactionReceipt, Function, @@ -149,6 +167,7 @@ impl Display for Type { } Type::Mapping(k, v) => write!(f, "mapping({} => {})", k, v), Type::Contract(ContractInfo(name, _)) => write!(f, "{}", name), + Type::Event(event) => write!(f, "{}", event.full_signature()), Type::Function => write!(f, "function"), Type::Transaction => write!(f, "Transaction"), @@ -182,6 +201,7 @@ impl> From for NonParametricType { Type::Tuple(_) => NonParametricType::Tuple, Type::Mapping(..) => NonParametricType::Mapping, Type::Contract(..) => NonParametricType::Contract, + Type::Event(..) => NonParametricType::Event, Type::Function => NonParametricType::Function, Type::Transaction => NonParametricType::Transaction, Type::TransactionReceipt => NonParametricType::TransactionReceipt, @@ -494,9 +514,17 @@ impl Type { abi.functions.keys().map(|s| s.to_string()).collect() } Type::NamedTuple(_, fields) => fields.0.keys().map(|s| s.to_string()).collect(), - Type::Type(type_) => STATIC_METHODS - .get(&type_.into()) - .map_or(vec![], |m| m.keys().cloned().collect()), + Type::Type(type_) => { + let mut static_methods = STATIC_METHODS + .get(&type_.into()) + .map_or(vec![], |m| m.keys().cloned().collect()); + + if let Type::Contract(ContractInfo(_, abi)) = type_.as_ref() { + static_methods.extend(abi.events.keys().map(|s| s.to_string())); + } + + static_methods + } _ => INSTANCE_METHODS .get(&self.into()) .map_or(vec![], |m| m.keys().cloned().collect()), diff --git a/src/interpreter/value.rs b/src/interpreter/value.rs index bb5268c..1be2ab6 100644 --- a/src/interpreter/value.rs +++ b/src/interpreter/value.rs @@ -521,6 +521,7 @@ impl Value { } Value::Contract(c, addr) => c.make_function(member, *addr).map(Into::into), Value::Func(f) => f.member_access(member), + Value::TypeObject(Type::Contract(c)) => c.member_access(member), _ => { let (type_, methods) = match self { Value::TypeObject(Type::Type(type_)) => {