From d0d82f651da6b25f0653f22ed47db00c3c8642d0 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 4 Sep 2026 12:48:53 +0530 Subject: [PATCH 01/10] Impl evm invoke --- docs/docs/users/reference/cli.sh | 1 + src/cli/subcommands/evm_cmd.rs | 132 ++++++++++++++++++++++++++----- src/lotus_json/mod.rs | 16 +++- 3 files changed, 127 insertions(+), 22 deletions(-) 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..b8e25ceba7cf 100644 --- a/src/cli/subcommands/evm_cmd.rs +++ b/src/cli/subcommands/evm_cmd.rs @@ -1,21 +1,25 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use crate::eth::EAMMethod; +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 +45,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, in attoFIL + #[arg(long, default_value_t = 0)] + value: u64, + /// Filecoin address of the contract + address: Address, + /// Hex-encoded ABI calldata + calldata: EthBytes, + }, /// Simulate an eth contract call Call { /// Ethereum sender address @@ -61,11 +78,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 +115,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)?) + .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 +143,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 +181,73 @@ async fn deploy( Ok(()) } +async fn invoke( + client: rpc::Client, + from: Option
, + value: u64, + 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: TokenAmount::from_atto(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?; + + anyhow::ensure!( + lookup.receipt.exit_code().is_success(), + "actor execution failed" + ); + + println!("Gas used: {}", lookup.receipt.gas_used()); + + 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 +279,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/lotus_json/mod.rs b/src/lotus_json/mod.rs index 4af7ffd1f688..c56b76136a68 100644 --- a/src/lotus_json/mod.rs +++ b/src/lotus_json/mod.rs @@ -410,8 +410,14 @@ pub mod hexify_vec_bytes { where D: Deserializer<'de>, { - let s = String::deserialize(deserializer)?; - let s = Cow::from(s.strip_prefix("0x").unwrap_or(&s)); + let raw = String::deserialize(deserializer)?; + let trimmed = raw.trim(); + let s = Cow::from( + trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + .unwrap_or(trimmed), + ); // 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 +730,10 @@ mod tests { ("0xF", vec![15]), ("0x2a42", vec![42, 66]), ("0x2A42", vec![42, 66]), + ("0X2a42", vec![42, 66]), + (" 0x2a42\n", vec![42, 66]), + ("\t0X2A42 ", vec![42, 66]), + ("2a42", vec![42, 66]), ]; for (input, expected) in cases.into_iter() { @@ -733,7 +743,7 @@ mod tests { self::assert_eq!(deserialized, expected); } - let fail_cases = ["cthulhu", "x", "0xazathoth"]; + let fail_cases = ["cthulhu", "x", "0xazathoth", "0x2a 42", "0x2a\n42"]; for input in fail_cases.into_iter() { let deserializer: StringDeserializer = String::from_str(input).unwrap().into_deserializer(); From 7be6dcef9c7e45e0bb0210f8d90ec6fdb70e7a7f Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 4 Sep 2026 13:04:29 +0530 Subject: [PATCH 02/10] use forest evm invoke in test --- .../developers/guides/rpc_stateful_tests.md | 2 - .../subcommands/devnet_cmd/eth_skip_sender.rs | 38 +++++++++---------- src/dev/subcommands/tests_cmd/helpers.rs | 5 +++ 3 files changed, 23 insertions(+), 22 deletions(-) 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/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs index e16a2dae0c3c..c4335adb949f 100644 --- a/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs +++ b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs @@ -362,44 +362,42 @@ async fn lotus_send( from: &Address, to: &Address, calldata: &[u8], - gas_limit: Option, + 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?; - } + 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(cid) } -async fn invoke(to: &Address, calldata: &[u8]) -> anyhow::Result { - lotus_send(deployer().await?, to, calldata, None).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(()) } async fn submit_at_gas_limit( @@ -408,7 +406,7 @@ async fn submit_at_gas_limit( calldata: &[u8], gas_limit: u64, ) -> anyhow::Result<()> { - lotus_send(from, to, calldata, Some(gas_limit)).await?; + lotus_send(from, to, calldata, gas_limit).await?; Ok(()) } 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 From c79966688309cfc929d414f96f1093cca8d985ac Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 4 Sep 2026 13:26:59 +0530 Subject: [PATCH 03/10] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1fe7021d6a5..38c691aabdd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ ### Added - [#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 From d01df04aa5ab5ce8898a6cb99e0b4f1048aa85bb Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 4 Sep 2026 14:03:33 +0530 Subject: [PATCH 04/10] use i64 for value --- src/cli/subcommands/evm_cmd.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/subcommands/evm_cmd.rs b/src/cli/subcommands/evm_cmd.rs index b8e25ceba7cf..91ba37ad6b19 100644 --- a/src/cli/subcommands/evm_cmd.rs +++ b/src/cli/subcommands/evm_cmd.rs @@ -52,7 +52,7 @@ pub enum EvmCommands { from: Option
, /// Value to send with the invocation message, in attoFIL #[arg(long, default_value_t = 0)] - value: u64, + value: i64, /// Filecoin address of the contract address: Address, /// Hex-encoded ABI calldata @@ -184,7 +184,7 @@ async fn deploy( async fn invoke( client: rpc::Client, from: Option
, - value: u64, + value: i64, address: Address, calldata: EthBytes, ) -> anyhow::Result<()> { From 08dbee540edebca193f1ceade78ddea76f5f816a Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 8 Sep 2026 13:44:38 +0530 Subject: [PATCH 05/10] revert input handling --- src/lotus_json/mod.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/lotus_json/mod.rs b/src/lotus_json/mod.rs index c56b76136a68..4af7ffd1f688 100644 --- a/src/lotus_json/mod.rs +++ b/src/lotus_json/mod.rs @@ -410,14 +410,8 @@ pub mod hexify_vec_bytes { where D: Deserializer<'de>, { - let raw = String::deserialize(deserializer)?; - let trimmed = raw.trim(); - let s = Cow::from( - trimmed - .strip_prefix("0x") - .or_else(|| trimmed.strip_prefix("0X")) - .unwrap_or(trimmed), - ); + let s = String::deserialize(deserializer)?; + let s = Cow::from(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`. @@ -730,10 +724,6 @@ mod tests { ("0xF", vec![15]), ("0x2a42", vec![42, 66]), ("0x2A42", vec![42, 66]), - ("0X2a42", vec![42, 66]), - (" 0x2a42\n", vec![42, 66]), - ("\t0X2A42 ", vec![42, 66]), - ("2a42", vec![42, 66]), ]; for (input, expected) in cases.into_iter() { @@ -743,7 +733,7 @@ mod tests { self::assert_eq!(deserialized, expected); } - let fail_cases = ["cthulhu", "x", "0xazathoth", "0x2a 42", "0x2a\n42"]; + let fail_cases = ["cthulhu", "x", "0xazathoth"]; for input in fail_cases.into_iter() { let deserializer: StringDeserializer = String::from_str(input).unwrap().into_deserializer(); From b0292046c7adba8b132f7434948f3fcd3657eafa Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 8 Sep 2026 14:43:53 +0530 Subject: [PATCH 06/10] use TokenAmount --- src/cli/subcommands/evm_cmd.rs | 40 +++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/cli/subcommands/evm_cmd.rs b/src/cli/subcommands/evm_cmd.rs index 91ba37ad6b19..f241768cf5e4 100644 --- a/src/cli/subcommands/evm_cmd.rs +++ b/src/cli/subcommands/evm_cmd.rs @@ -21,6 +21,7 @@ use clap::Subcommand; use fil_actor_eam_state::v16::CreateExternalParams; use fil_actor_evm_state::v16::{InvokeContractParams, InvokeContractReturn}; use fvm_ipld_encoding::RawBytes; +use num::{BigInt, Signed as _}; use std::path::PathBuf; use std::str::FromStr as _; use std::time::Duration; @@ -50,9 +51,9 @@ pub enum EvmCommands { /// Optionally specify the account to use for sending the exec message #[arg(long)] from: Option
, - /// Value to send with the invocation message, in attoFIL - #[arg(long, default_value_t = 0)] - value: i64, + /// Value to send with the invocation message + #[arg(long, value_parser = parse_invoke_value, default_value = "0")] + value: TokenAmount, /// Filecoin address of the contract address: Address, /// Hex-encoded ABI calldata @@ -184,7 +185,7 @@ async fn deploy( async fn invoke( client: rpc::Client, from: Option
, - value: i64, + value: TokenAmount, address: Address, calldata: EthBytes, ) -> anyhow::Result<()> { @@ -197,7 +198,7 @@ async fn invoke( let msg = Message { to: address, from, - value: TokenAmount::from_atto(value), + value, method_num: EVMMethod::InvokeContract as u64, params, ..Default::default() @@ -279,3 +280,32 @@ async fn call( } } } + +/// Bare digits are `attoFIL`; otherwise parse a human amount (e.g. `1FIL`) and reject negatives. +fn parse_invoke_value(s: &str) -> anyhow::Result { + if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) { + return Ok(TokenAmount::from_atto(BigInt::from_str(s)?)); + } + let amount = crate::cli::humantoken::parse(s)?; + anyhow::ensure!(!amount.is_negative(), "value cannot be negative"); + Ok(amount) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case("1000", Ok(TokenAmount::from_atto(1000)))] + #[case("1FIL", Ok(TokenAmount::from_whole(1)))] + #[case("1attoFIL", Ok(TokenAmount::from_atto(1)))] + #[case("-1", Err("value cannot be negative"))] + fn parse_invoke_value_cases(#[case] input: &str, #[case] expected: Result) { + let result = parse_invoke_value(input).map_err(|e| e.to_string()); + match expected { + Ok(amount) => assert_eq!(result.unwrap(), amount), + Err(msg) => assert!(result.unwrap_err().contains(msg)), + } + } +} From 1e199fccccd7bb436cd81f3204bf21ed20fc1ea7 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 10 Sep 2026 15:20:49 +0530 Subject: [PATCH 07/10] fmt --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c691aabdd5..0be48d1777d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ ### Added - [#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 From 73e5f421abd0e9044e124f4f3b131b5f210c9c15 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 10 Sep 2026 16:14:01 +0530 Subject: [PATCH 08/10] cleanup --- src/cli/subcommands/evm_cmd.rs | 40 +++---------------- .../subcommands/devnet_cmd/eth_skip_sender.rs | 18 ++------- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/src/cli/subcommands/evm_cmd.rs b/src/cli/subcommands/evm_cmd.rs index f241768cf5e4..50c69155f3ea 100644 --- a/src/cli/subcommands/evm_cmd.rs +++ b/src/cli/subcommands/evm_cmd.rs @@ -1,6 +1,7 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use crate::cli::humantoken; use crate::eth::{EAMMethod, EVMMethod}; use crate::rpc::eth::{ BlockNumberOrHash, Predefined, @@ -21,7 +22,6 @@ use clap::Subcommand; use fil_actor_eam_state::v16::CreateExternalParams; use fil_actor_evm_state::v16::{InvokeContractParams, InvokeContractReturn}; use fvm_ipld_encoding::RawBytes; -use num::{BigInt, Signed as _}; use std::path::PathBuf; use std::str::FromStr as _; use std::time::Duration; @@ -51,8 +51,8 @@ pub enum EvmCommands { /// Optionally specify the account to use for sending the exec message #[arg(long)] from: Option
, - /// Value to send with the invocation message - #[arg(long, value_parser = parse_invoke_value, default_value = "0")] + /// 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, @@ -213,13 +213,14 @@ async fn invoke( 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" ); - println!("Gas used: {}", lookup.receipt.gas_used()); - let ret: InvokeContractReturn = from_slice_with_fallback(lookup.receipt.return_data().bytes()) .context("evm result not correctly encoded")?; if ret.output_data.is_empty() { @@ -280,32 +281,3 @@ async fn call( } } } - -/// Bare digits are `attoFIL`; otherwise parse a human amount (e.g. `1FIL`) and reject negatives. -fn parse_invoke_value(s: &str) -> anyhow::Result { - if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) { - return Ok(TokenAmount::from_atto(BigInt::from_str(s)?)); - } - let amount = crate::cli::humantoken::parse(s)?; - anyhow::ensure!(!amount.is_negative(), "value cannot be negative"); - Ok(amount) -} - -#[cfg(test)] -mod tests { - use super::*; - use rstest::rstest; - - #[rstest] - #[case("1000", Ok(TokenAmount::from_atto(1000)))] - #[case("1FIL", Ok(TokenAmount::from_whole(1)))] - #[case("1attoFIL", Ok(TokenAmount::from_atto(1)))] - #[case("-1", Err("value cannot be negative"))] - fn parse_invoke_value_cases(#[case] input: &str, #[case] expected: Result) { - let result = parse_invoke_value(input).map_err(|e| e.to_string()); - match expected { - Ok(amount) => assert_eq!(result.unwrap(), amount), - Err(msg) => assert!(result.unwrap_err().contains(msg)), - } - } -} diff --git a/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs index c4335adb949f..e768bd8f1a68 100644 --- a/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs +++ b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs @@ -363,7 +363,7 @@ async fn lotus_send( to: &Address, calldata: &[u8], gas_limit: u64, -) -> anyhow::Result { +) -> anyhow::Result<()> { let forest = forest_client()?; let from_s = from.to_string(); let to_s = to.to_string(); @@ -391,7 +391,7 @@ async fn lotus_send( wait_for_cid(&forest, cid) .await .with_context(|| format!("transaction submitted at eth_estimateGas {gas_limit} failed"))?; - Ok(cid) + Ok(()) } async fn invoke(to: &Address, calldata: &[u8]) -> anyhow::Result<()> { @@ -400,16 +400,6 @@ async fn invoke(to: &Address, calldata: &[u8]) -> anyhow::Result<()> { Ok(()) } -async fn submit_at_gas_limit( - from: &Address, - to: &Address, - calldata: &[u8], - gas_limit: u64, -) -> anyhow::Result<()> { - lotus_send(from, to, calldata, gas_limit).await?; - Ok(()) -} - async fn eth_call_msg( client: &Client, msg: EthCallMessage, @@ -912,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))?; @@ -965,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<()> { From cc98141a37c57bc4ab8895ef570a4fbb2c10349b Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 10 Sep 2026 16:31:27 +0530 Subject: [PATCH 09/10] trim input --- src/cli/subcommands/evm_cmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/subcommands/evm_cmd.rs b/src/cli/subcommands/evm_cmd.rs index 50c69155f3ea..04bd93cdb544 100644 --- a/src/cli/subcommands/evm_cmd.rs +++ b/src/cli/subcommands/evm_cmd.rs @@ -116,7 +116,7 @@ async fn deploy( ) -> anyhow::Result<()> { let mut initcode = std::fs::read(&contract).context("failed to read contract")?; if is_hex { - initcode = EthBytes::from_str(std::str::from_utf8(&initcode)?) + initcode = EthBytes::from_str(std::str::from_utf8(&initcode)?.trim()) .context("failed to decode contract")? .0; } From a0cb8f2b6711657416869c0674c347cbe2feb865 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 10 Sep 2026 16:37:57 +0530 Subject: [PATCH 10/10] support 0X prefix --- src/lotus_json/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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() {