Skip to content

Commit

Permalink
Merge pull request #13 from danhper/customize-block
Browse files Browse the repository at this point in the history
Customize block
  • Loading branch information
danhper authored Aug 1, 2024
2 parents c87711a + 3c08043 commit 6092e54
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 25 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 22 additions & 0 deletions docs/src/builtin_values.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions docs/src/interacting_with_contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/interpreter/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
21 changes: 21 additions & 0 deletions src/interpreter/builtins/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ fn debug(env: &mut Env, _receiver: &Value, args: &[Value]) -> Result<Value> {
}
}

fn block(env: &mut Env, _receiver: &Value, args: &[Value]) -> Result<Value> {
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<Value> {
let cmd = args
.first()
Expand Down Expand Up @@ -185,6 +196,16 @@ lazy_static! {
debug,
vec![vec![], vec![FunctionParam::new("debug", Type::Bool)]]
);
pub static ref REPL_BLOCK: Arc<dyn FunctionDef> = 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<dyn FunctionDef> = SyncMethod::arc(
"exec",
exec,
Expand Down
23 changes: 12 additions & 11 deletions src/interpreter/env.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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,
};
use url::Url;

use alloy::{
eips::BlockId,
network::{AnyNetwork, Ethereum, EthereumWallet, NetworkWallet, TxSigner},
primitives::Address,
providers::{Provider, ProviderBuilder},
Expand All @@ -24,9 +25,9 @@ pub struct Env {
variables: Vec<HashMap<String, Value>>,
types: HashMap<String, Type>,
provider: Arc<dyn Provider<Http<Client>, Ethereum>>,
function_bodies: HashMap<String, Statement>,
wallet: Option<EthereumWallet>,
ledger: Option<Arc<Mutex<Ledger>>>,
block_id: BlockId,
pub config: Config,
}

Expand All @@ -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: BlockId::latest(),
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());
}
Expand All @@ -71,6 +64,14 @@ impl Env {
self.config.debug
}

pub fn set_block(&mut self, block: BlockId) {
self.block_id = block;
}

pub fn block(&self) -> BlockId {
self.block_id
}

pub fn get_provider(&self) -> Arc<dyn Provider<Http<Client>, Ethereum>> {
self.provider.clone()
}
Expand Down
81 changes: 67 additions & 14 deletions src/interpreter/functions/contract.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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<Box<Value>>,
block: Option<BlockId>,
from: Option<Address>,
}

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<H: std::hash::Hasher>(&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 {
Expand All @@ -72,6 +98,8 @@ impl TryFrom<&HashableIndexMap<String, Value>> 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),
}
}
Expand Down Expand Up @@ -184,7 +212,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(&addr, func, &call_options, env).await
} else {
_execute_contract_send(&addr, func, &call_options, env).await
}
Expand All @@ -193,6 +221,28 @@ impl FunctionDef for ContractFunction {
}
}

fn _build_transaction<T, P, N>(
addr: &Address,
func: &CallBuilder<T, P, alloy::json_abi::Function, N>,
opts: &CallOptions,
) -> Result<TransactionRequest>
where
T: Transport + Clone,
P: Provider<T, N>,
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<T, P, N>(
addr: &Address,
func: CallBuilder<T, P, alloy::json_abi::Function, N>,
Expand All @@ -204,34 +254,37 @@ where
P: Provider<T, N>,
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?;
Ok(Value::Transaction(*tx.tx_hash()))
}

async fn _execute_contract_call<T, P, N>(
addr: &Address,
func: CallBuilder<T, P, alloy::json_abi::Function, N>,
opts: &CallOptions,
env: &Env,
) -> Result<Value>
where
T: Transport + Clone,
P: Provider<T, N>,
N: Network,
{
let result = func.call().await?;
let mut tx_req = _build_transaction(addr, &func, opts)?;
if let Some(from_) = opts.from {
tx_req = tx_req.with_from(from_);
}
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)
Expand Down
31 changes: 31 additions & 0 deletions src/interpreter/value.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use alloy::{
dyn_abi::DynSolValue,
eips::{BlockId, BlockNumberOrTag},
hex,
primitives::{Address, B256, I256, U256},
};
Expand All @@ -9,6 +10,7 @@ use itertools::Itertools;
use std::{
fmt::{self, Display, Formatter},
ops::{Add, Div, Mul, Rem, Sub},
str::FromStr,
};

use super::{
Expand Down Expand Up @@ -225,6 +227,22 @@ impl From<Function> for Value {
}
}

impl From<BlockId> 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<const N: usize> From<alloy::primitives::FixedBytes<N>> for Value {
fn from(bytes: alloy::primitives::FixedBytes<N>) -> Self {
Value::FixBytes(B256::from_slice(&bytes[..]), N)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -404,6 +426,15 @@ impl Value {
}
}

pub fn as_block_id(&self) -> Result<BlockId> {
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<String, Value>> {
match self {
Value::NamedTuple(_, map) => Ok(map),
Expand Down

0 comments on commit 6092e54

Please sign in to comment.