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_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/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..a86d340 100644 --- a/docs/src/interacting_with_contracts.md +++ b/docs/src/interacting_with_contracts.md @@ -59,4 +59,39 @@ 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. + +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/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..5fa4882 100644 --- a/src/interpreter/builtins/mod.rs +++ b/src/interpreter/builtins/mod.rs @@ -9,6 +9,8 @@ mod address; mod block; mod concat; mod console; +mod event; +mod events; mod format; mod iterable; mod misc; @@ -29,6 +31,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 +135,14 @@ 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("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()); 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..41759b1 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::{ @@ -5,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; @@ -60,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)] @@ -85,12 +102,14 @@ pub enum NonParametricType { Tuple, Mapping, Contract, + Event, Transaction, TransactionReceipt, Function, Repl, Block, Console, + Events, Abi, Type, } @@ -112,12 +131,14 @@ pub enum Type { Tuple(Vec), Mapping(Box, Box), Contract(ContractInfo), + Event(alloy::json_abi::Event), Transaction, TransactionReceipt, Function, Repl, Block, Console, + Events, Abi, Type(Box), } @@ -146,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"), @@ -153,6 +175,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), @@ -178,12 +201,14 @@ 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, Type::Repl => NonParametricType::Repl, Type::Block => NonParametricType::Block, Type::Console => NonParametricType::Console, + Type::Events => NonParametricType::Events, Type::Abi => NonParametricType::Abi, Type::Type(_) => NonParametricType::Type, } @@ -489,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()), @@ -537,6 +570,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..1be2ab6 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())), @@ -506,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_)) => { @@ -551,15 +567,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 +585,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())), ), ]);