From 127484df8e2b74ee6214ee362ce03d586a7e73db Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Thu, 1 Aug 2024 00:59:44 +0100 Subject: [PATCH 1/2] Add block ID to env --- src/interpreter/env.rs | 23 ++++++++++++----------- src/interpreter/functions/contract.rs | 9 +++++++-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/interpreter/env.rs b/src/interpreter/env.rs index 6146edb..79b63e7 100644 --- a/src/interpreter/env.rs +++ b/src/interpreter/env.rs @@ -1,5 +1,5 @@ use futures_util::lock::Mutex; -use solang_parser::pt::{Expression, Identifier, Statement}; +use solang_parser::pt::{Expression, Identifier}; use std::{ collections::{HashMap, HashSet}, sync::Arc, @@ -7,6 +7,7 @@ use std::{ use url::Url; use alloy::{ + eips::BlockId, network::{AnyNetwork, Ethereum, EthereumWallet, NetworkWallet, TxSigner}, primitives::Address, providers::{Provider, ProviderBuilder}, @@ -24,9 +25,9 @@ pub struct Env { variables: Vec>, types: HashMap, provider: Arc, Ethereum>>, - function_bodies: HashMap, wallet: Option, ledger: Option>>, + block_id: Option, pub config: Config, } @@ -40,21 +41,13 @@ impl Env { variables: vec![HashMap::new()], types: HashMap::new(), provider: Arc::new(provider), - function_bodies: HashMap::new(), wallet: None, ledger: None, + block_id: None, config, } } - pub fn set_function_body(&mut self, name: &str, body: Statement) { - self.function_bodies.insert(name.to_string(), body); - } - - pub fn get_function_body(&self, name: &str) -> Option<&Statement> { - self.function_bodies.get(name) - } - pub fn push_scope(&mut self) { self.variables.push(HashMap::new()); } @@ -71,6 +64,14 @@ impl Env { self.config.debug } + pub fn set_block(&mut self, block: BlockId) { + self.block_id = Some(block); + } + + pub fn block(&self) -> Option { + self.block_id + } + pub fn get_provider(&self) -> Arc, Ethereum>> { self.provider.clone() } diff --git a/src/interpreter/functions/contract.rs b/src/interpreter/functions/contract.rs index 97325e6..a3706b1 100644 --- a/src/interpreter/functions/contract.rs +++ b/src/interpreter/functions/contract.rs @@ -184,7 +184,7 @@ impl FunctionDef for ContractFunction { } else if self.mode == ContractCallMode::Call || (self.mode == ContractCallMode::Default && is_view) { - _execute_contract_call(func).await + _execute_contract_call(env, func).await } else { _execute_contract_send(&addr, func, &call_options, env).await } @@ -224,6 +224,7 @@ where } async fn _execute_contract_call( + env: &Env, func: CallBuilder, ) -> Result where @@ -231,7 +232,11 @@ where P: Provider, N: Network, { - let result = func.call().await?; + let mut call = func.call(); + if let Some(b) = env.block() { + call = call.block(b); + } + let result = call.await?; let return_values = result .into_iter() .map(Value::try_from) From 3c08043c92b9ed90f6551d3d9c0ca95518867634 Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Thu, 1 Aug 2024 11:22:08 +0100 Subject: [PATCH 2/2] Add logic to customize block through repl object and options --- CHANGELOG.md | 8 +++ docs/src/builtin_values.md | 22 +++++++ docs/src/interacting_with_contracts.md | 15 +++++ src/interpreter/builtins/mod.rs | 1 + src/interpreter/builtins/repl.rs | 21 +++++++ src/interpreter/env.rs | 8 +-- src/interpreter/functions/contract.rs | 84 ++++++++++++++++++++------ src/interpreter/value.rs | 31 ++++++++++ 8 files changed, 168 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b331bd6..5ba62a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Not released + +### Features + +* Add `repl.block()` +* Allow to customize `block` through call options +* Allow to customize `from` through call options + ## 0.1.1 (2024-07-30) ### Features diff --git a/docs/src/builtin_values.md b/docs/src/builtin_values.md index 61cdb4e..ecd31e4 100644 --- a/docs/src/builtin_values.md +++ b/docs/src/builtin_values.md @@ -87,6 +87,28 @@ If the [RPC URL](./configuration.md#rpc-url) is set in the configuration file, t >> repl.rpc("optimism") ``` +### `repl.block() -> uint256 | string` + +Returns the current block in use for contract calls. + +```javascript +>> repl.block() +"latest" +``` + +### `repl.block(uint256 number) | repl.block(string tag) | repl.block(bytes32 hash)` + + +Sets the block to use for contract calls. +Can be a number, a tag (e.g. "latest" or "safe"), or a block hash. + +```javascript +>> repl.block(123436578) +>> repl.block() +123436578 +``` + + ### `repl.exec(string command) -> uint256` Executes a command in the shell, displays the output and returns the exit code. diff --git a/docs/src/interacting_with_contracts.md b/docs/src/interacting_with_contracts.md index 31a6f10..ebdedc8 100644 --- a/docs/src/interacting_with_contracts.md +++ b/docs/src/interacting_with_contracts.md @@ -28,6 +28,21 @@ Transaction(0x6a2f1b956769d06257475d18ceeec9ee9487d91c97d36346a3cc84d568e36e5c) Transaction(0xf3e85039345ff864bb216b10e84c7d009e99ec55b370dae22706b0d48ea41583) ``` +### Transaction options + +There are different options available when calling and sending transactions to contracts. +The options can be passed using the `{key: value}` Solidity syntax, for example: + +```javascript +>> tx = weth.deposit{value: 1e18}() +``` + +The following options are currently supported: + +* `value`: sets the `msg.value` of the transaction +* `block`: sets the block number to execute the call on (only works for calls, not for sending transactions) +* `from`: sets the `from` for the call (only works for calls, not for sending transactions) + ## Transaction receipts After sending a transaction, you can get the transaction receipt using the `Transaction.getReceipt` method. diff --git a/src/interpreter/builtins/mod.rs b/src/interpreter/builtins/mod.rs index 1386196..65cb6d0 100644 --- a/src/interpreter/builtins/mod.rs +++ b/src/interpreter/builtins/mod.rs @@ -137,6 +137,7 @@ lazy_static! { repl_methods.insert("connected".to_string(), repl::REPL_IS_CONNECTED.clone()); repl_methods.insert("rpc".to_string(), repl::REPL_RPC.clone()); repl_methods.insert("debug".to_string(), repl::REPL_DEBUG.clone()); + repl_methods.insert("block".to_string(), repl::REPL_BLOCK.clone()); repl_methods.insert("exec".to_string(), repl::REPL_EXEC.clone()); repl_methods.insert("loadAbi".to_string(), repl::REPL_LOAD_ABI.clone()); repl_methods.insert("fetchAbi".to_string(), repl::REPL_FETCH_ABI.clone()); diff --git a/src/interpreter/builtins/repl.rs b/src/interpreter/builtins/repl.rs index c191976..29961cb 100644 --- a/src/interpreter/builtins/repl.rs +++ b/src/interpreter/builtins/repl.rs @@ -63,6 +63,17 @@ fn debug(env: &mut Env, _receiver: &Value, args: &[Value]) -> Result { } } +fn block(env: &mut Env, _receiver: &Value, args: &[Value]) -> Result { + match args { + [] => Ok(env.block().into()), + [value] => { + env.set_block(value.as_block_id()?); + Ok(Value::Null) + } + _ => bail!("block: invalid arguments"), + } +} + fn exec(_env: &mut Env, _receiver: &Value, args: &[Value]) -> Result { let cmd = args .first() @@ -185,6 +196,16 @@ lazy_static! { debug, vec![vec![], vec![FunctionParam::new("debug", Type::Bool)]] ); + pub static ref REPL_BLOCK: Arc = SyncMethod::arc( + "block", + block, + vec![ + vec![], + vec![FunctionParam::new("block", Type::Uint(256))], + vec![FunctionParam::new("block", Type::String)], + vec![FunctionParam::new("block", Type::FixBytes(32))], + ] + ); pub static ref REPL_EXEC: Arc = SyncMethod::arc( "exec", exec, diff --git a/src/interpreter/env.rs b/src/interpreter/env.rs index 79b63e7..0db7af3 100644 --- a/src/interpreter/env.rs +++ b/src/interpreter/env.rs @@ -27,7 +27,7 @@ pub struct Env { provider: Arc, Ethereum>>, wallet: Option, ledger: Option>>, - block_id: Option, + block_id: BlockId, pub config: Config, } @@ -43,7 +43,7 @@ impl Env { provider: Arc::new(provider), wallet: None, ledger: None, - block_id: None, + block_id: BlockId::latest(), config, } } @@ -65,10 +65,10 @@ impl Env { } pub fn set_block(&mut self, block: BlockId) { - self.block_id = Some(block); + self.block_id = block; } - pub fn block(&self) -> Option { + pub fn block(&self) -> BlockId { self.block_id } diff --git a/src/interpreter/functions/contract.rs b/src/interpreter/functions/contract.rs index a3706b1..0881858 100644 --- a/src/interpreter/functions/contract.rs +++ b/src/interpreter/functions/contract.rs @@ -1,7 +1,8 @@ -use std::sync::Arc; +use std::{hash::Hash, sync::Arc}; use alloy::{ contract::{CallBuilder, ContractInstance, Interface}, + eips::BlockId, json_abi::StateMutability, network::{Network, TransactionBuilder}, primitives::{keccak256, Address, FixedBytes}, @@ -49,9 +50,34 @@ impl TryFrom<&str> for ContractCallMode { } } -#[derive(Debug, Clone, Default, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CallOptions { value: Option>, + block: Option, + from: Option
, +} + +impl CallOptions { + pub fn validate_send(&self) -> Result<()> { + if self.block.is_some() { + bail!("block is only available for calls"); + } else if self.from.is_some() { + bail!("from is only available for calls"); + } else { + Ok(()) + } + } +} + +impl Hash for CallOptions { + fn hash(&self, state: &mut H) { + self.value.hash(state); + match self.block { + Some(BlockId::Hash(h)) => h.block_hash.hash(state), + Some(BlockId::Number(n)) => n.hash(state), + None => 0.hash(state), + } + } } impl std::fmt::Display for CallOptions { @@ -72,6 +98,8 @@ impl TryFrom<&HashableIndexMap> for CallOptions { for (k, v) in value.0.iter() { match k.as_str() { "value" => opts.value = Some(Box::new(v.clone())), + "block" => opts.block = Some(v.as_block_id()?), + "from" => opts.from = Some(v.as_address()?), _ => bail!("unexpected key {}", k), } } @@ -184,7 +212,7 @@ impl FunctionDef for ContractFunction { } else if self.mode == ContractCallMode::Call || (self.mode == ContractCallMode::Default && is_view) { - _execute_contract_call(env, func).await + _execute_contract_call(&addr, func, &call_options, env).await } else { _execute_contract_send(&addr, func, &call_options, env).await } @@ -193,6 +221,28 @@ impl FunctionDef for ContractFunction { } } +fn _build_transaction( + addr: &Address, + func: &CallBuilder, + opts: &CallOptions, +) -> Result +where + T: Transport + Clone, + P: Provider, + N: Network, +{ + let data = func.calldata(); + let input = TransactionInput::new(data.clone()); + + let mut tx_req = TransactionRequest::default().with_to(*addr).input(input); + if let Some(value) = opts.value.as_ref() { + let value = value.as_u256()?; + tx_req = tx_req.with_value(value); + } + + Ok(tx_req) +} + async fn _execute_contract_send( addr: &Address, func: CallBuilder, @@ -204,19 +254,12 @@ where P: Provider, N: Network, { - let data = func.calldata(); - let input = TransactionInput::new(data.clone()); + opts.validate_send()?; + let mut tx_req = _build_transaction(addr, &func, opts)?; let from_ = env .get_default_sender() .ok_or(anyhow!("no wallet connected"))?; - let mut tx_req = TransactionRequest::default() - .with_from(from_) - .with_to(*addr) - .input(input); - if let Some(value) = opts.value.as_ref() { - let value = value.as_u256()?; - tx_req = tx_req.with_value(value); - } + tx_req = tx_req.with_from(from_); let provider = env.get_provider(); let tx = provider.send_transaction(tx_req).await?; @@ -224,19 +267,24 @@ where } async fn _execute_contract_call( - env: &Env, + addr: &Address, func: CallBuilder, + opts: &CallOptions, + env: &Env, ) -> Result where T: Transport + Clone, P: Provider, N: Network, { - let mut call = func.call(); - if let Some(b) = env.block() { - call = call.block(b); + let mut tx_req = _build_transaction(addr, &func, opts)?; + if let Some(from_) = opts.from { + tx_req = tx_req.with_from(from_); } - let result = call.await?; + let block = opts.block.unwrap_or(env.block()); + let provider = env.get_provider(); + let return_bytes = provider.call(&tx_req).block(block).await?; + let result = func.decode_output(return_bytes, true)?; let return_values = result .into_iter() .map(Value::try_from) diff --git a/src/interpreter/value.rs b/src/interpreter/value.rs index 2c41c26..cf2823d 100644 --- a/src/interpreter/value.rs +++ b/src/interpreter/value.rs @@ -1,5 +1,6 @@ use alloy::{ dyn_abi::DynSolValue, + eips::{BlockId, BlockNumberOrTag}, hex, primitives::{Address, B256, I256, U256}, }; @@ -9,6 +10,7 @@ use itertools::Itertools; use std::{ fmt::{self, Display, Formatter}, ops::{Add, Div, Mul, Rem, Sub}, + str::FromStr, }; use super::{ @@ -225,6 +227,22 @@ impl From for Value { } } +impl From for Value { + fn from(block_id: BlockId) -> Self { + match block_id { + BlockId::Hash(hash) => Value::FixBytes(hash.block_hash, 32), + BlockId::Number(n) => match n { + BlockNumberOrTag::Earliest => Value::Str("earliest".to_string()), + BlockNumberOrTag::Latest => Value::Str("latest".to_string()), + BlockNumberOrTag::Pending => Value::Str("pending".to_string()), + BlockNumberOrTag::Number(n) => Value::Uint(U256::from(n), 256), + BlockNumberOrTag::Finalized => Value::Str("finalized".to_string()), + BlockNumberOrTag::Safe => Value::Str("safe".to_string()), + }, + } + } +} + impl From> for Value { fn from(bytes: alloy::primitives::FixedBytes) -> Self { Value::FixBytes(B256::from_slice(&bytes[..]), N) @@ -345,6 +363,10 @@ impl Value { } } + pub fn is_number(&self) -> bool { + matches!(self, Value::Uint(..) | Value::Int(..)) + } + pub fn is_builtin(&self) -> bool { matches!( self, @@ -404,6 +426,15 @@ impl Value { } } + pub fn as_block_id(&self) -> Result { + match self { + Value::FixBytes(hash, 32) => Ok(BlockId::Hash((*hash).into())), + Value::Str(s) => BlockId::from_str(s).map_err(Into::into), + n if n.is_number() => Ok(BlockId::number(n.as_u64()?)), + _ => bail!("cannot convert {} to block id", self.get_type()), + } + } + pub fn as_record(&self) -> Result<&HashableIndexMap> { match self { Value::NamedTuple(_, map) => Ok(map),