Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

- [#7471](https://github.com/ChainSafe/forest/issues/7471): Implement `forest-cli evm deploy` and `forest-cli evm call`.

- [#7595](https://github.com/ChainSafe/forest/pull/7595): Implement `forest-cli evm invoke`.
Comment thread
sudo-shashank marked this conversation as resolved.

### Changed

- [#7594](https://github.com/ChainSafe/forest/pull/7594): `forest-cli index backfill` now defaults `--recompute` to true and exits with an error if any tipsets were skipped.
Expand Down
2 changes: 0 additions & 2 deletions docs/docs/developers/guides/rpc_stateful_tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,3 @@ pub async fn test_eth_method(client: Arc<Client>) -> anyhow::Result<()> {
## Notes

The current test framework assumes a running node and a valid wallet.

Consider implementing `forest-tool evm deploy` and `forest-tool evm invoke` subcommands to simplify contract deployment and test invocation.
1 change: 1 addition & 0 deletions docs/docs/users/reference/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ generate_markdown_section "forest-cli" "f3 ready"

generate_markdown_section "forest-cli" "evm"
generate_markdown_section "forest-cli" "evm deploy"
generate_markdown_section "forest-cli" "evm invoke"
generate_markdown_section "forest-cli" "evm call"

generate_markdown_section "forest-tool" ""
Expand Down
134 changes: 115 additions & 19 deletions src/cli/subcommands/evm_cmd.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use crate::eth::EAMMethod;
use crate::cli::humantoken;
use crate::eth::{EAMMethod, EVMMethod};
use crate::rpc::eth::{
BlockNumberOrHash, Predefined,
types::{EthAddress, EthBytes, EthCallMessage},
};
use crate::rpc::types::MessageLookup;
use crate::rpc::{self, prelude::*};
use crate::shim::actors::eam;
use crate::shim::address::Address;
use crate::shim::econ::TokenAmount;
use crate::shim::message::Message;
use crate::utils::encoding::{from_slice_with_fallback, hex};
use anyhow::Context as _;
use base64::Engine as _;
use base64::prelude::BASE64_STANDARD;
use cid::Cid;
use clap::Subcommand;
use fil_actor_eam_state::v16::CreateExternalParams;
use fil_actor_evm_state::v16::{InvokeContractParams, InvokeContractReturn};
use fvm_ipld_encoding::RawBytes;
use std::path::PathBuf;
use std::str::FromStr as _;
Expand All @@ -41,6 +46,19 @@ pub enum EvmCommands {
/// Contract init code
contract: PathBuf,
},
/// Invoke an EVM smart contract using the specified calldata
Invoke {
/// Optionally specify the account to use for sending the exec message
#[arg(long)]
from: Option<Address>,
/// Value to send with the invocation message (human FIL amount, e.g. `1FIL`, `1attoFIL`)
#[arg(long, value_parser = humantoken::parse, default_value = "0")]
value: TokenAmount,
/// Filecoin address of the contract
address: Address,
/// Hex-encoded ABI calldata
calldata: EthBytes,
},
/// Simulate an eth contract call
Call {
/// Ethereum sender address
Expand All @@ -61,11 +79,34 @@ impl EvmCommands {
wait,
contract,
} => deploy(client, from, hex, wait.unwrap_or(true), contract).await,
Self::Invoke {
from,
value,
address,
calldata,
} => invoke(client, from, value, address, calldata).await,
Self::Call { from, to, params } => call(client, from, to, params).await,
}
}
}

async fn resolve_from(client: &rpc::Client, from: Option<Address>) -> anyhow::Result<Address> {
match from {
Some(addr) => Ok(addr),
None => WalletDefaultAddress::call(client, ())
.await?
.context("no default wallet address"),
}
}

async fn wait_for_message(client: &rpc::Client, cid: Cid) -> anyhow::Result<MessageLookup> {
println!("waiting for message to execute...");
client
.call(StateWaitMsg::request((cid, 0, WAIT_LOOKBACK, true))?.with_timeout(WAIT_TIMEOUT))
.await
.context("error waiting for message")
}

async fn deploy(
client: rpc::Client,
from: Option<Address>,
Expand All @@ -75,15 +116,12 @@ async fn deploy(
) -> anyhow::Result<()> {
let mut initcode = std::fs::read(&contract).context("failed to read contract")?;
if is_hex {
initcode = decode_hex_contract(&initcode).context("failed to decode contract")?;
initcode = EthBytes::from_str(std::str::from_utf8(&initcode)?.trim())
.context("failed to decode contract")?
.0;
}

let from = match from {
Some(addr) => addr,
None => WalletDefaultAddress::call(&client, ())
.await?
.context("no default wallet address")?,
};
let from = resolve_from(&client, from).await?;

let params = RawBytes::serialize(CreateExternalParams(initcode))
.context("failed to serialize Create params")?;
Expand All @@ -106,11 +144,7 @@ async fn deploy(
return Ok(());
}

println!("waiting for message to execute...");
let lookup = client
.call(StateWaitMsg::request((cid, 0, WAIT_LOOKBACK, true))?.with_timeout(WAIT_TIMEOUT))
.await
.context("error waiting for message")?;
let lookup = wait_for_message(&client, cid).await?;

println!("Exit Code: {}", lookup.receipt.exit_code().value());
println!("Gas Used: {}", lookup.receipt.gas_used());
Expand Down Expand Up @@ -148,6 +182,74 @@ async fn deploy(
Ok(())
}

async fn invoke(
client: rpc::Client,
from: Option<Address>,
value: TokenAmount,
address: Address,
calldata: EthBytes,
) -> anyhow::Result<()> {
let from = resolve_from(&client, from).await?;
let params = RawBytes::serialize(InvokeContractParams {
input_data: calldata.0,
})
.context("failed to encode evm params as cbor")?;

let msg = Message {
to: address,
from,
value,
method_num: EVMMethod::InvokeContract as u64,
params,
..Default::default()
};

println!("sending message...");
let smsg = MpoolPushMessage::call(&client, (msg, None))
.await
.context("failed to push message")?;
let cid = smsg.cid();
println!("Message CID: {cid}");

let lookup = wait_for_message(&client, cid).await?;

println!("Exit Code: {}", lookup.receipt.exit_code().value());
println!("Gas Used: {}", lookup.receipt.gas_used());

anyhow::ensure!(
Comment thread
sudo-shashank marked this conversation as resolved.
lookup.receipt.exit_code().is_success(),
"actor execution failed"
);

let ret: InvokeContractReturn = from_slice_with_fallback(lookup.receipt.return_data().bytes())
.context("evm result not correctly encoded")?;
if ret.output_data.is_empty() {
println!("OK");
} else {
println!("{}", hex::encode(&ret.output_data));
}

if let Some(root) = lookup.receipt.events_root() {
let events = ChainGetEvents::call(&client, (root,))
.await
.context("failed to load events")?;
println!("Events emitted:");
for event in events {
println!("\tEmitter ID: {}", event.emitter);
for entry in event.entries {
println!(
"\t\tKey: {}, Value: 0x{}, Flags: b{:b}",
entry.key,
hex::encode(&entry.value.0),
entry.flags
);
}
}
}

Ok(())
}

async fn call(
client: rpc::Client,
from: EthAddress,
Expand Down Expand Up @@ -179,9 +281,3 @@ async fn call(
}
}
}

fn decode_hex_contract(raw: &[u8]) -> anyhow::Result<Vec<u8>> {
let s = std::str::from_utf8(raw)?.trim();
let s = s.strip_prefix("0X").unwrap_or(s);
Ok(EthBytes::from_str(s)?.0)
}
52 changes: 20 additions & 32 deletions src/dev/subcommands/devnet_cmd/eth_skip_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,53 +362,41 @@ async fn lotus_send(
from: &Address,
to: &Address,
calldata: &[u8],
gas_limit: Option<u64>,
) -> anyhow::Result<Cid> {
gas_limit: u64,
) -> anyhow::Result<()> {
let forest = forest_client()?;
let from_s = from.to_string();
let to_s = to.to_string();
let params = hex::encode(calldata);
let gas = gas_limit.map(|g| g.to_string());
let mut args = vec![
let gas = gas_limit.to_string();
let out = lotus_exec_retrying_transient(&[
"send",
"--from",
from_s.as_str(),
"--params-hex",
params.as_str(),
];
if let Some(gas) = gas.as_deref() {
args.extend(["--gas-limit", gas]);
}
args.extend([to_s.as_str(), "0"]);
let out = lotus_exec_retrying_transient(&args).await?;
"--gas-limit",
gas.as_str(),
to_s.as_str(),
"0",
])
.await?;
let cid = Cid::from_str(
out.lines()
.last()
.context("no cid from `lotus send`")?
.trim(),
)?;
if let Some(limit) = gas_limit {
eprintln!("submitted at estimate {limit}: {cid}");
wait_for_cid(&forest, cid)
.await
.with_context(|| format!("transaction submitted at eth_estimateGas {limit} failed"))?;
} else {
wait_for_cid(&forest, cid).await?;
}
Ok(cid)
}

async fn invoke(to: &Address, calldata: &[u8]) -> anyhow::Result<Cid> {
lotus_send(deployer().await?, to, calldata, None).await
eprintln!("submitted at estimate {gas_limit}: {cid}");
wait_for_cid(&forest, cid)
.await
.with_context(|| format!("transaction submitted at eth_estimateGas {gas_limit} failed"))?;
Ok(())
}

async fn submit_at_gas_limit(
from: &Address,
to: &Address,
calldata: &[u8],
gas_limit: u64,
) -> anyhow::Result<()> {
lotus_send(from, to, calldata, Some(gas_limit)).await?;
async fn invoke(to: &Address, calldata: &[u8]) -> anyhow::Result<()> {
let from = deployer().await?.to_string();
forest_evm_invoke(&from, &to.to_string(), &hex::encode(calldata))?;
Ok(())
}

Expand Down Expand Up @@ -914,7 +902,7 @@ async fn round_trip_from_unfunded() -> anyhow::Result<()> {
actor.sequence
);

submit_at_gas_limit(&from.f4, &coin.f4, &calldata, gas).await?;
lotus_send(&from.f4, &coin.f4, &calldata, gas).await?;
let after = get_actor(&forest, from.f4)
.await?
.with_context(|| format!("actor {} missing after successful submit", from.f4))?;
Expand Down Expand Up @@ -967,7 +955,7 @@ async fn round_trip_recursive() -> anyhow::Result<()> {
);

fund_on_chain(&from.cli, RECURSIVE_FUND_AMT).await?;
submit_at_gas_limit(&from.f4, &nested.f4, &calldata, gas).await
lotus_send(&from.f4, &nested.f4, &calldata, gas).await
}

async fn call_sender_identity() -> anyhow::Result<()> {
Expand Down
5 changes: 5 additions & 0 deletions src/dev/subcommands/tests_cmd/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,11 @@ pub fn forest_evm_deploy_hex(from: &str, bytecode_hex: &str) -> anyhow::Result<S
forest_cli(&["evm", "deploy", "--from", from, "--hex", path])
}

/// Invoke a contract with `forest-cli evm invoke`.
pub fn forest_evm_invoke(from: &str, to: &str, calldata_hex: &str) -> anyhow::Result<String> {
forest_cli(&["evm", "invoke", "--from", from, to, calldata_hex])
}

/// Parse the `f4 Address:` line from `forest-cli evm deploy` output.
pub fn parse_f4_from_evm_deploy(out: &str) -> anyhow::Result<Address> {
let f4 = out
Expand Down
7 changes: 6 additions & 1 deletion src/lotus_json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,11 @@ pub mod hexify_vec_bytes {
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let s = Cow::from(s.strip_prefix("0x").unwrap_or(&s));
let s = Cow::from(
s.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.unwrap_or(&s),
);

// Pad with 0 if odd length. This is necessary because decoding requires an even
// number of characters, whereas a valid input is also `0x0`.
Expand Down Expand Up @@ -724,6 +728,7 @@ mod tests {
("0xF", vec![15]),
("0x2a42", vec![42, 66]),
("0x2A42", vec![42, 66]),
("0X2a42", vec![42, 66]),
];

for (input, expected) in cases.into_iter() {
Expand Down
Loading