diff --git a/CHANGELOG.md b/CHANGELOG.md index a1fe7021d6a5..0be48d1777d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. + ### 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. diff --git a/docs/docs/developers/guides/rpc_stateful_tests.md b/docs/docs/developers/guides/rpc_stateful_tests.md index e5e50850704a..231bef401c89 100644 --- a/docs/docs/developers/guides/rpc_stateful_tests.md +++ b/docs/docs/developers/guides/rpc_stateful_tests.md @@ -71,5 +71,3 @@ pub async fn test_eth_method(client: Arc) -> 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. diff --git a/docs/docs/users/reference/cli.sh b/docs/docs/users/reference/cli.sh index f5049ed3a0b5..0eb96c449f34 100755 --- a/docs/docs/users/reference/cli.sh +++ b/docs/docs/users/reference/cli.sh @@ -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" "" diff --git a/src/cli/subcommands/evm_cmd.rs b/src/cli/subcommands/evm_cmd.rs index 7523121293a9..04bd93cdb544 100644 --- a/src/cli/subcommands/evm_cmd.rs +++ b/src/cli/subcommands/evm_cmd.rs @@ -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 _; @@ -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
, + /// 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 @@ -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
) -> anyhow::Result
{ + 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 { + 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
, @@ -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")?; @@ -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()); @@ -148,6 +182,74 @@ async fn deploy( Ok(()) } +async fn invoke( + client: rpc::Client, + from: Option
, + 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!( + 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, @@ -179,9 +281,3 @@ async fn call( } } } - -fn decode_hex_contract(raw: &[u8]) -> anyhow::Result> { - let s = std::str::from_utf8(raw)?.trim(); - let s = s.strip_prefix("0X").unwrap_or(s); - Ok(EthBytes::from_str(s)?.0) -} diff --git a/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs index e16a2dae0c3c..e768bd8f1a68 100644 --- a/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs +++ b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs @@ -362,53 +362,41 @@ async fn lotus_send( from: &Address, to: &Address, calldata: &[u8], - gas_limit: Option, -) -> anyhow::Result { + 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 { - 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(()) } @@ -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))?; @@ -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<()> { diff --git a/src/dev/subcommands/tests_cmd/helpers.rs b/src/dev/subcommands/tests_cmd/helpers.rs index e11e442f88f0..37d3c906e1f1 100644 --- a/src/dev/subcommands/tests_cmd/helpers.rs +++ b/src/dev/subcommands/tests_cmd/helpers.rs @@ -489,6 +489,11 @@ pub fn forest_evm_deploy_hex(from: &str, bytecode_hex: &str) -> anyhow::Result anyhow::Result { + 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
{ let f4 = out diff --git a/src/lotus_json/mod.rs b/src/lotus_json/mod.rs index 4af7ffd1f688..36c21841d961 100644 --- a/src/lotus_json/mod.rs +++ b/src/lotus_json/mod.rs @@ -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`. @@ -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() {