From 6eab3e8b6da2c800b3a76e66607b5537d61f3e31 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 11 Sep 2026 07:50:10 +0530 Subject: [PATCH 1/3] add --method and --params-hex support --- CHANGELOG.md | 2 + docs/docs/users/reference/cli.md | 4 + src/cli/subcommands/evm_cmd.rs | 9 +- src/dev/subcommands/devnet_cmd/eth_gas.rs | 80 +++----- .../subcommands/devnet_cmd/eth_skip_sender.rs | 69 ++----- src/dev/subcommands/tests_cmd/helpers.rs | 85 +++++---- src/dev/subcommands/tests_cmd/wallet.rs | 53 ++++++ src/eth/transaction.rs | 23 ++- src/wallet/subcommands/wallet_cmd.rs | 180 +++++++++++++----- 9 files changed, 305 insertions(+), 200 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be48d1777d6..c1528662ca36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ - [#7595](https://github.com/ChainSafe/forest/pull/7595): Implement `forest-cli evm invoke`. +- [#7472](https://github.com/ChainSafe/forest/issues/7472): `forest-wallet send` now accepts `--method` and `--params-hex`, matching `lotus send`. + ### 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/users/reference/cli.md b/docs/docs/users/reference/cli.md index 57a04dcb615f..035f3801624b 100644 --- a/docs/docs/users/reference/cli.md +++ b/docs/docs/users/reference/cli.md @@ -341,6 +341,10 @@ Options: Wait for the message to be on chain with the given confidence by calling `StateWaitMsg`. The command waits until the message has been on chain for at least `confidence` epochs --wait-timeout Timeout duration for `--wait-confidence`, e.g. `30s`, `5m`. If not set, the timeout will be `confidence + 5` epochs + --method + Specify method to invoke (default: 0) + --params-hex + Specify invocation parameters in hex -h, --help Print help ``` diff --git a/src/cli/subcommands/evm_cmd.rs b/src/cli/subcommands/evm_cmd.rs index 04bd93cdb544..10857193ea47 100644 --- a/src/cli/subcommands/evm_cmd.rs +++ b/src/cli/subcommands/evm_cmd.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use crate::cli::humantoken; -use crate::eth::{EAMMethod, EVMMethod}; +use crate::eth::{EAMMethod, EVMMethod, encode_evm_params}; use crate::rpc::eth::{ BlockNumberOrHash, Predefined, types::{EthAddress, EthBytes, EthCallMessage}, @@ -20,7 +20,7 @@ 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 fil_actor_evm_state::v16::InvokeContractReturn; use fvm_ipld_encoding::RawBytes; use std::path::PathBuf; use std::str::FromStr as _; @@ -190,10 +190,7 @@ async fn invoke( 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 params = RawBytes::new(encode_evm_params(&calldata.0)?); let msg = Message { to: address, diff --git a/src/dev/subcommands/devnet_cmd/eth_gas.rs b/src/dev/subcommands/devnet_cmd/eth_gas.rs index 28431e9ad179..894aa927fbbe 100644 --- a/src/dev/subcommands/devnet_cmd/eth_gas.rs +++ b/src/dev/subcommands/devnet_cmd/eth_gas.rs @@ -20,7 +20,6 @@ use crate::rpc::prelude::*; use crate::shim::address::Address; use crate::utils::encoding::{hex, keccak_256}; use anyhow::{Context as _, ensure}; -use cid::Cid; use jsonrpsee::core::ClientError; use libtest_mimic::{Arguments, Failed, Trial}; use std::str::FromStr as _; @@ -100,10 +99,9 @@ fn recurse_calldata(depth: u64) -> Vec { out } -/// Deployed `NestedGas` addresses: `eth` for the JSON-RPC calls, `f4` as the `lotus send` target. +/// Deployed `NestedGas` ETH address for JSON-RPC and `forest-wallet send`. struct Deployed { eth: EthAddress, - f4: Address, } /// Deploys `NestedGas` once per process. @@ -111,8 +109,8 @@ async fn contract() -> anyhow::Result<&'static Deployed> { static CONTRACT: OnceCell = OnceCell::const_new(); CONTRACT .get_or_try_init(|| async { - let from = sender_addr().await?.to_string(); - let deploy = forest_evm_deploy_hex(&from, NESTED_GAS_HEX)?; + let from = sender().await?; + let deploy = forest_evm_deploy_hex(from, NESTED_GAS_HEX)?; let f4 = parse_f4_from_evm_deploy(&deploy)?; eprintln!("deployed NestedGas at {f4}"); poll_until_actor_on("forest", f4, forest_client).await?; @@ -120,17 +118,15 @@ async fn contract() -> anyhow::Result<&'static Deployed> { poll_until_next_epoch().await?; anyhow::Ok(Deployed { eth: EthAddress::from_filecoin_address(&f4)?, - f4, }) }) .await } -/// An `f4` sender funded well enough to afford the gas limits under test. Lotus rejects -/// estimation from an unfunded or non-`f4` sender, so both properties are required. -async fn sender_addr() -> anyhow::Result<&'static Address> { - static SENDER: OnceCell
= OnceCell::const_new(); - SENDER +/// Funded delegated sender. Lotus rejects estimates from an unfunded or non-`f4` address. +async fn sender() -> anyhow::Result<&'static str> { + static SENDER: OnceCell = OnceCell::const_new(); + Ok(SENDER .get_or_try_init(|| async { let addr = lotus_exec(&["wallet", "new", "delegated"])?; let msg = send_from( @@ -142,12 +138,13 @@ async fn sender_addr() -> anyhow::Result<&'static Address> { eprintln!("funding sender {addr} with {SENDER_FUND_AMT}, msg: {msg}"); let balance = poll_until_funded(&addr, Backend::Local).await?; eprintln!("sender {addr} funded balance: {balance}"); - let sender = Address::from_str(&addr).context("parsing the sender address")?; - poll_until_actor_on("lotus", sender, lotus_client).await?; + let parsed = Address::from_str(&addr).context("parsing the sender address")?; + poll_until_actor_on("lotus", parsed, lotus_client).await?; import_lotus_wallet_into_forest(&addr)?; - Ok(sender) + Ok(addr) }) - .await + .await? + .as_str()) } async fn estimate( @@ -155,9 +152,10 @@ async fn estimate( calldata: Vec, block: BlockNumberOrHash, ) -> anyhow::Result { - let (sender, deployed) = tokio::try_join!(sender_addr(), contract())?; + let (from, deployed) = tokio::try_join!(sender(), contract())?; + let from = Address::from_str(from).context("parsing the sender address")?; let msg = EthCallMessage { - from: Some(EthAddress::from_filecoin_address(sender)?), + from: Some(EthAddress::from_filecoin_address(&from)?), to: Some(deployed.eth), data: Some(EthBytes(calldata)), ..Default::default() @@ -194,7 +192,7 @@ async fn poll_until_next_epoch() -> anyhow::Result<()> { /// height only after the deploy/fund guarantees the pinned tipset already contains the contract and /// sender on both nodes (the funding poll also lets both catch up to the deploy). async fn pinned_common_block() -> anyhow::Result<(Client, Client, i64)> { - tokio::try_join!(contract(), sender_addr())?; + tokio::try_join!(contract(), sender())?; let (forest_c, lotus_c) = (forest_client()?, lotus_client()?); let block = common_block_number(&forest_c, &lotus_c).await?; Ok((forest_c, lotus_c, block)) @@ -235,46 +233,26 @@ async fn estimate_agrees(depth: u64) -> anyhow::Result<()> { async fn estimate_is_sufficient_on_chain() -> anyhow::Result<()> { let forest = forest_client()?; // No cross-node comparison here, so `Latest` is fine: the estimate must reflect the same - // fresh state the following `lotus send` executes against. + // fresh state the following `forest-wallet send` executes against. let estimate = estimate( &forest, recurse_calldata(NESTED_DEPTH), BlockNumberOrHash::PredefinedBlock(Predefined::Latest), ) .await?; - let sender = sender_addr().await?.to_string(); - let target = contract().await?.f4.to_string(); - let params = hex::encode(recurse_calldata(NESTED_DEPTH)); - let gas_limit = estimate.to_string(); - // `lotus send` infers `InvokeContract` and CBOR-wraps the params when the sender is an - // eth account, and rejects an explicit `--method`, so pass the bare calldata. Retry the - // submit while Lotus's mpool briefly lags the freshly funded sender. - let out = lotus_exec_retrying_transient(&[ - "send", - "--from", - &sender, - "--params-hex", - ¶ms, - "--gas-limit", - &gas_limit, - &target, - "0", - ]) - .await?; - let cid = out - .lines() - .last() - .context("no cid from `lotus send`")? - .trim(); + let from = sender().await?; + let target = hex::encode_prefixed(contract().await?.eth.0.as_bytes()); + // `forest-wallet send` infers `InvokeContract` and CBOR-wraps the params when the sender is an + // eth account, and rejects an explicit `--method`, so pass the bare calldata. + let cid = wallet_send_calldata(from, &target, &recurse_calldata(NESTED_DEPTH), estimate) + .await + .with_context(|| { + format!( + "a transaction submitted at forest's own eth_estimateGas value ({estimate}) failed \ + on chain; the estimate is not a usable gas limit" + ) + })?; eprintln!("submitted at forest's estimate {estimate}: {cid}"); - - let lookup = poll_until_message_executed(&forest, Cid::from_str(cid)?).await?; - let exit = lookup.receipt.exit_code(); - ensure!( - exit.is_success(), - "a transaction submitted at forest's own eth_estimateGas value ({estimate}) failed \ - on chain with exit code {exit}; the estimate is not a usable gas limit" - ); Ok(()) } diff --git a/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs index e768bd8f1a68..a3489322cf1d 100644 --- a/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs +++ b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs @@ -25,7 +25,6 @@ use crate::shim::econ::TokenAmount; use crate::shim::state_tree::ActorState; use crate::utils::encoding::{hex, keccak_256}; use anyhow::{Context as _, ensure}; -use cid::Cid; use jsonrpsee::core::ClientError; use libtest_mimic::{Arguments, Failed, Trial}; use std::str::FromStr as _; @@ -193,7 +192,7 @@ fn latest() -> BlockNumberOrHash { BlockNumberOrHash::PredefinedBlock(Predefined::Latest) } -/// Deployed EVM actor: `eth` for JSON-RPC, `f4` for `lotus send` / `StateGetActor`. +/// Deployed EVM actor: `eth` for JSON-RPC and `forest-wallet send`, `f4` for `StateGetActor`. #[derive(Clone, Copy)] struct Deployed { eth: EthAddress, @@ -348,52 +347,6 @@ async fn fund_on_chain(cli_addr: &str, amount: &str) -> anyhow::Result
Ok(addr) } -async fn wait_for_cid(forest: &Client, cid: Cid) -> anyhow::Result<()> { - let lookup = poll_until_message_executed(forest, cid).await?; - let exit = lookup.receipt.exit_code(); - ensure!( - exit.is_success(), - "message {cid} failed on chain with exit code {exit}" - ); - Ok(()) -} - -async fn lotus_send( - from: &Address, - to: &Address, - calldata: &[u8], - 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.to_string(); - let out = lotus_exec_retrying_transient(&[ - "send", - "--from", - from_s.as_str(), - "--params-hex", - params.as_str(), - "--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(), - )?; - 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 invoke(to: &Address, calldata: &[u8]) -> anyhow::Result<()> { let from = deployer().await?.to_string(); forest_evm_invoke(&from, &to.to_string(), &hex::encode(calldata))?; @@ -902,7 +855,15 @@ async fn round_trip_from_unfunded() -> anyhow::Result<()> { actor.sequence ); - lotus_send(&from.f4, &coin.f4, &calldata, gas).await?; + let cid = wallet_send_calldata( + &from.cli, + &hex::encode_prefixed(coin.eth.0.as_bytes()), + &calldata, + gas, + ) + .await + .with_context(|| format!("transaction submitted at eth_estimateGas {gas} failed"))?; + eprintln!("submitted at estimate {gas}: {cid}"); let after = get_actor(&forest, from.f4) .await? .with_context(|| format!("actor {} missing after successful submit", from.f4))?; @@ -955,7 +916,15 @@ async fn round_trip_recursive() -> anyhow::Result<()> { ); fund_on_chain(&from.cli, RECURSIVE_FUND_AMT).await?; - lotus_send(&from.f4, &nested.f4, &calldata, gas).await + wallet_send_calldata( + &from.cli, + &hex::encode_prefixed(nested.eth.0.as_bytes()), + &calldata, + gas, + ) + .await + .with_context(|| format!("transaction submitted at eth_estimateGas {gas} failed"))?; + Ok(()) } 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 37d3c906e1f1..50e573eb6a3f 100644 --- a/src/dev/subcommands/tests_cmd/helpers.rs +++ b/src/dev/subcommands/tests_cmd/helpers.rs @@ -8,7 +8,7 @@ use std::str::FromStr as _; use std::sync::LazyLock; use std::time::Duration; -use anyhow::{Context as _, bail}; +use anyhow::{Context as _, bail, ensure}; use cid::Cid; use jsonrpsee::core::ClientError; use serde_json::{Value, json}; @@ -22,6 +22,7 @@ use crate::shim::address::Address; use crate::shim::clock::ChainEpoch; use crate::shim::state_tree::ActorState; use crate::state_manager::FAILED_TO_LOAD_MESSAGE; +use crate::utils::encoding::hex; /// Funded preloaded address from env `FOREST_TEST_PRELOADED_ADDRESS` (`forest_wallet_init` in `scripts/tests/harness.sh`). pub static FOREST_TEST_PRELOADED_ADDRESS: LazyLock = LazyLock::new(|| { @@ -124,7 +125,7 @@ pub fn balance(address: &str, backend: Backend) -> anyhow::Result { /// Send with `--from`. `backend` chooses the signing keystore /// (local file vs `--remote-wallet`). pub fn send_from(from: &str, to: &str, amount: &str, backend: Backend) -> anyhow::Result { - send_from_and_maybe_wait(from, to, amount, backend, true) + wallet_send(backend, from, to, amount, &[], true) } pub fn send_from_no_wait( @@ -133,23 +134,69 @@ pub fn send_from_no_wait( amount: &str, backend: Backend, ) -> anyhow::Result { - send_from_and_maybe_wait(from, to, amount, backend, false) + wallet_send(backend, from, to, amount, &[], false) } -fn send_from_and_maybe_wait( +/// `forest-wallet send` with optional extra flags. When `wait` is set, uses +/// `--wait-confidence 0 --wait-timeout 10m`. +pub fn wallet_send( + backend: Backend, from: &str, to: &str, amount: &str, - backend: Backend, + extra: &[&str], wait: bool, ) -> anyhow::Result { let mut args = vec!["send", to, amount, "--from", from]; + args.extend_from_slice(extra); if wait { args.extend(["--wait-confidence", "0", "--wait-timeout", "10m"]); } wallet(backend, &args) } +/// Parse the CID from `forest-wallet send` and require a successful on-chain receipt. +/// `send` with `--wait-confidence` already waited for inclusion; this checks the exit code. +pub async fn assert_send_ok(out: &str) -> anyhow::Result { + let cid = Cid::from_str( + out.lines() + .last() + .context("no cid from `forest-wallet send`")? + .trim(), + )?; + let lookup = poll_until_message_executed(&forest_client()?, cid).await?; + let exit = lookup.receipt.exit_code(); + ensure!( + exit.is_success(), + "message {cid} failed on chain with exit code {exit}" + ); + Ok(cid) +} + +/// Submit an EVM contract call via `forest-wallet send --params-hex`. +/// Imports `from` from the Lotus keystore into Forest's remote wallet when needed. +pub async fn wallet_send_calldata( + from: &str, + to: &str, + calldata: &[u8], + gas_limit: u64, +) -> anyhow::Result { + if wallet(Backend::Remote, &["has", from])? != "true" { + import_lotus_wallet_into_forest(from)?; + } + let params = hex::encode(calldata); + let gas = gas_limit.to_string(); + let out = wallet_send( + Backend::Remote, + from, + to, + "0", + &["--params-hex", params.as_str(), "--gas-limit", gas.as_str()], + true, + )?; + assert_send_ok(&out).await +} + /// Max attempts for [`rpc_call_with_retry`]. const RPC_RETRIES: usize = 3; /// Delay between [`rpc_call_with_retry`] attempts; one block-time at calibnet @@ -229,34 +276,6 @@ pub async fn poll_until_actor_on( .await } -/// True for a `lotus` CLI failure that clears once the node catches up to the funding block (the -/// Lotus node trails Forest by a block on the forest-produced devnet). -fn is_transient_lotus_error(e: &anyhow::Error) -> bool { - let msg = format!("{e:#}"); - [ - "check has failed", - "failed to get nonce from mempool", - "actor not found", - "resolution lookup failed", - "not enough funds", - ] - .iter() - .any(|s| msg.contains(s)) -} - -/// Run a `lotus` command, retrying while it fails with a transient error (see -/// [`is_transient_lotus_error`]). Any other failure propagates immediately. -pub async fn lotus_exec_retrying_transient(args: &[&str]) -> anyhow::Result { - poll(&format!("lotus {}", args.join(" ")), || async { - match lotus_exec(args) { - Ok(out) => Ok(Some(out)), - Err(e) if is_transient_lotus_error(&e) => Ok(None), - Err(e) => Err(e), - } - }) - .await -} - /// Delegated signer: create once on local, fund locally, mirror to remote /// for tests that query or sign. pub async fn funded_delegated_addr() -> &'static str { diff --git a/src/dev/subcommands/tests_cmd/wallet.rs b/src/dev/subcommands/tests_cmd/wallet.rs index 0b5d491915c2..a62e1fe183d1 100644 --- a/src/dev/subcommands/tests_cmd/wallet.rs +++ b/src/dev/subcommands/tests_cmd/wallet.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0, MIT use super::helpers::*; +use crate::rpc::eth::types::EthAddress; +use crate::utils::encoding::{hex, keccak_256}; use libtest_mimic::{Arguments, Trial}; +use std::str::FromStr as _; /// Wallet integration tests #[derive(Debug, clap::Args)] @@ -76,6 +79,10 @@ fn tests() -> Vec { block_on(delegated_remote_send()); Ok(()) }), + Trial::test("send_params_hex_invokes_contract", || { + block_on(send_params_hex_invokes_contract()); + Ok(()) + }), ] } @@ -218,3 +225,49 @@ async fn delegated_remote_send() { "{target} balance unchanged after delegated --remote-wallet send: {observed}", ); } + +const SIMPLE_COIN_HEX: &str = include_str!("../devnet_cmd/contracts/simple_coin/simple_coin.hex"); + +async fn send_params_hex_invokes_contract() { + let from = funded_delegated_addr().await; + // Deploying an EVM actor costs more gas than the shared delegated seed. + let fund = send_from( + &FOREST_TEST_PRELOADED_ADDRESS, + from, + "1 FIL", + Backend::Local, + ) + .unwrap(); + eprintln!("funding delegated {from} for evm deploy, msg: {fund}"); + let deploy = forest_evm_deploy_hex(from, SIMPLE_COIN_HEX).unwrap(); + let f4 = parse_f4_from_evm_deploy(&deploy).unwrap(); + eprintln!("deployed SimpleCoin at {f4}"); + poll_until_actor_on("forest", f4, forest_client) + .await + .unwrap(); + + let eth = EthAddress::from_filecoin_address(&f4).unwrap(); + let target = hex::encode_prefixed(eth.0.as_bytes()); + let from_eth = + EthAddress::from_filecoin_address(&crate::shim::address::Address::from_str(from).unwrap()) + .unwrap(); + let mut calldata = keccak_256(b"getBalance(address)") + .get(..4) + .expect("keccak256 is 32 bytes") + .to_vec(); + calldata.extend_from_slice(&[0u8; 12]); + calldata.extend_from_slice(from_eth.0.as_bytes()); + let params = hex::encode(&calldata); + + let out = wallet_send( + Backend::Local, + from, + &target, + "0", + &["--params-hex", ¶ms], + true, + ) + .unwrap(); + let cid = assert_send_ok(&out).await.unwrap(); + eprintln!("send --params-hex {cid}"); +} diff --git a/src/eth/transaction.rs b/src/eth/transaction.rs index 6a374796fc48..e07e8adfddb5 100644 --- a/src/eth/transaction.rs +++ b/src/eth/transaction.rs @@ -469,20 +469,25 @@ pub struct MethodInfo { pub params: Vec, } +/// CBOR-encode ABI calldata as an FVM byte array (`InvokeContract` / `CreateExternal` params). +/// Empty input stays empty (no CBOR wrapper). +pub fn encode_evm_params(input: &[u8]) -> anyhow::Result> { + if input.is_empty() { + return Ok(Vec::new()); + } + cbor4ii::serde::to_vec( + Vec::with_capacity(input.len()), + &Value::Bytes(input.to_vec()), + ) + .context("failed to encode params") +} + /// Retrieves method info pub fn get_filecoin_method_info( recipient: Option<&EthAddress>, input: &[u8], ) -> anyhow::Result { - let params = if !input.is_empty() { - cbor4ii::serde::to_vec( - Vec::with_capacity(input.len()), - &Value::Bytes(input.to_vec()), - ) - .context("failed to encode params")? - } else { - Vec::new() - }; + let params = encode_evm_params(input)?; let (to, method) = match recipient { None => { diff --git a/src/wallet/subcommands/wallet_cmd.rs b/src/wallet/subcommands/wallet_cmd.rs index 745156aaef6d..9087717d0c4b 100644 --- a/src/wallet/subcommands/wallet_cmd.rs +++ b/src/wallet/subcommands/wallet_cmd.rs @@ -14,7 +14,7 @@ use crate::key_management::{Key, KeyInfo}; use crate::{ ENCRYPTED_KEYSTORE_NAME, cli::humantoken, - eth::{EAMMethod, EVMMethod}, + eth::{EAMMethod, EVMMethod, encode_evm_params}, rpc::{ eth::{EthChainId, is_eth_address, types::EthAddress}, mpool::{MpoolGetNonce, MpoolPush, MpoolPushMessage}, @@ -39,6 +39,7 @@ use anyhow::{Context as _, bail}; use clap::Subcommand; use dialoguer::{Password, console::Term, theme::ColorfulTheme}; use directories::ProjectDirs; +use fvm_ipld_encoding::RawBytes; use jsonrpsee::core::ClientError; use num::Zero as _; use tabled::{builder::Builder, settings::Style}; @@ -321,6 +322,12 @@ pub enum WalletCommands { /// Timeout duration for `--wait-confidence`, e.g. `30s`, `5m`. If not set, the timeout will be `confidence + 5` epochs. #[arg(long, requires = "wait_confidence", value_parser = humantime::parse_duration)] wait_timeout: Option, + /// Specify method to invoke (default: 0) + #[arg(long)] + method: Option, + /// Specify invocation parameters in hex + #[arg(long)] + params_hex: Option, }, } impl WalletCommands { @@ -509,6 +516,8 @@ impl WalletCommands { gas_premium, wait_confidence, wait_timeout, + method, + params_hex, } => { let from: Address = match from { Some(a) => a.into(), @@ -532,13 +541,20 @@ impl WalletCommands { ) })?; } - let method_num = resolve_method_num(&from, &to, is_0x_recipient); + let invocation = resolve_send_invocation( + &from, + &to, + is_0x_recipient, + method, + params_hex.as_deref(), + )?; let message = Message { from, to, value: amount, - method_num, + method_num: invocation.method_num, + params: invocation.params, gas_limit: gas_limit as u64, gas_fee_cap: gas_feecap, gas_premium, @@ -679,14 +695,41 @@ fn wrap_frc0102(msg: &[u8]) -> Vec { [FRC_0102_FILECOIN_PREFIX, len.as_bytes(), msg].concat() } -fn resolve_method_num(from: &Address, to: &Address, is_0x_recipient: bool) -> u64 { - if !is_eth_address(from) && !is_0x_recipient { - return METHOD_SEND; - } - if *to == Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR { - EAMMethod::CreateExternal as u64 +#[derive(Debug)] +struct SendInvocation { + method_num: u64, + params: RawBytes, +} + +fn resolve_send_invocation( + from: &Address, + to: &Address, + is_0x_recipient: bool, + method: Option, + params_hex: Option<&str>, +) -> anyhow::Result { + let hex_bytes = params_hex + .map(|s| hex::decode(s).context("failed to decode hex params")) + .transpose()?; + let is_eth_path = is_eth_address(from) || is_0x_recipient; + + if is_eth_path { + if method.is_some() { + bail!("messages from f410f addresses may not specify a method number"); + } + Ok(SendInvocation { + method_num: if *to == Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR { + EAMMethod::CreateExternal as u64 + } else { + EVMMethod::InvokeContract as u64 + }, + params: RawBytes::new(encode_evm_params(hex_bytes.as_deref().unwrap_or_default())?), + }) } else { - EVMMethod::InvokeContract as u64 + Ok(SendInvocation { + method_num: method.unwrap_or(METHOD_SEND), + params: RawBytes::new(hex_bytes.unwrap_or_default()), + }) } } @@ -698,9 +741,10 @@ mod tests { use crate::rpc::eth::types::EthAddress; use crate::shim::address::{Address, CurrentNetwork, Network}; use crate::shim::message::METHOD_SEND; + use crate::utils::encoding::hex; use rstest::rstest; - use super::{SignatureType, resolve_method_num, resolve_target_address, wrap_frc0102}; + use super::{SignatureType, resolve_send_invocation, resolve_target_address, wrap_frc0102}; #[test] fn test_resolve_target_address_id() { @@ -765,58 +809,92 @@ mod tests { ); } - #[test] - fn test_resolve_method_num_send() { - let from = Address::from_str("f01234").unwrap(); - let to = Address::from_str("f01234").unwrap(); - let method = resolve_method_num(&from, &to, false); - assert_eq!(method, METHOD_SEND); - } - - #[test] - fn test_resolve_method_num_create_external() { - let from = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); - let to = Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR; - let method = resolve_method_num(&from, &to, false); - assert_eq!(method, EAMMethod::CreateExternal as u64); + #[rstest] + #[case::native_defaults("f01234", "f01234", false, METHOD_SEND)] + #[case::create_external( + "f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa", + "f010", + false, + EAMMethod::CreateExternal as u64 + )] + #[case::invoke_contract( + "f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa", + "f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa", + false, + EVMMethod::InvokeContract as u64 + )] + #[case::invoke_contract_eth( + "f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa", + "0x6cb414224f0b91de5c3b616e700e34a5172c149f", + true, + EVMMethod::InvokeContract as u64 + )] + #[case::native_to_delegated( + "f01234", + "f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa", + false, + METHOD_SEND + )] + #[case::native_to_eth( + "f01234", + "0x6cb414224f0b91de5c3b616e700e34a5172c149f", + true, + EVMMethod::InvokeContract as u64 + )] + fn test_resolve_send_invocation_method( + #[case] from: &str, + #[case] to: &str, + #[case] is_0x: bool, + #[case] expected: u64, + ) { + let from = Address::from_str(from).unwrap(); + let to = if is_0x { + EthAddress::from_str(to) + .unwrap() + .to_filecoin_address() + .unwrap() + } else { + Address::from_str(to).unwrap() + }; + let invocation = resolve_send_invocation(&from, &to, is_0x, None, None).unwrap(); + assert_eq!(invocation.method_num, expected); + assert!(invocation.params.is_empty()); } #[test] - fn test_resolve_method_num_invoke_contract() { + fn test_resolve_send_invocation_eth_wraps_params_hex() { let from = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); - let to = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); - let method = resolve_method_num(&from, &to, false); - assert_eq!(method, EVMMethod::InvokeContract as u64); + let eth = EthAddress::from_str("0x6cb414224f0b91de5c3b616e700e34a5172c149f").unwrap(); + let to = eth.to_filecoin_address().unwrap(); + let calldata = hex::decode("deadbeef").unwrap(); + let invocation = resolve_send_invocation(&from, &to, true, None, Some("deadbeef")).unwrap(); + assert_eq!(invocation.method_num, EVMMethod::InvokeContract as u64); + assert_eq!( + invocation.params.to_vec(), + crate::eth::get_filecoin_method_info(Some(ð), &calldata) + .unwrap() + .params + ); } #[test] - fn test_resolve_method_num_invoke_contract_eth() { + fn test_resolve_send_invocation_eth_rejects_method() { let from = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); - let to = EthAddress::from_str("0x6cb414224f0b91de5c3b616e700e34a5172c149f") - .unwrap() - .to_filecoin_address() - .unwrap(); - let method = resolve_method_num(&from, &to, true); - assert_eq!(method, EVMMethod::InvokeContract as u64); + let to = from; + let err = resolve_send_invocation(&from, &to, false, Some(0), None) + .unwrap_err() + .to_string(); + assert!(err.contains("may not specify a method number")); } #[test] - fn test_resolve_method_num_send_to_delegated() { + fn test_resolve_send_invocation_native_params_hex() { let from = Address::from_str("f01234").unwrap(); - let to = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); - let method = resolve_method_num(&from, &to, false); - assert_eq!(method, METHOD_SEND); - } - - #[test] - fn test_resolve_method_num_send_to_eth() { - let from = Address::from_str("f01234").unwrap(); - let to = EthAddress::from_str("0x6cb414224f0b91de5c3b616e700e34a5172c149f") - .unwrap() - .to_filecoin_address() - .unwrap(); - let method = resolve_method_num(&from, &to, true); - assert_eq!(method, EVMMethod::InvokeContract as u64); + let to = Address::from_str("f01234").unwrap(); + let invocation = + resolve_send_invocation(&from, &to, false, Some(2), Some("deadbeef")).unwrap(); + assert_eq!(invocation.method_num, 2); + assert_eq!(invocation.params.to_vec(), hex::decode("deadbeef").unwrap()); } #[rstest] From ebab0895890407f5a7433447d51cfe1e0bcf8351 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 11 Sep 2026 08:22:50 +0530 Subject: [PATCH 2/3] cleanup --- src/dev/subcommands/devnet_cmd/eth_gas.rs | 43 ++++----- .../subcommands/devnet_cmd/eth_skip_sender.rs | 91 ++++++++----------- src/dev/subcommands/tests_cmd/helpers.rs | 6 +- src/wallet/subcommands/wallet_cmd.rs | 4 +- 4 files changed, 61 insertions(+), 83 deletions(-) diff --git a/src/dev/subcommands/devnet_cmd/eth_gas.rs b/src/dev/subcommands/devnet_cmd/eth_gas.rs index 894aa927fbbe..7884204af51f 100644 --- a/src/dev/subcommands/devnet_cmd/eth_gas.rs +++ b/src/dev/subcommands/devnet_cmd/eth_gas.rs @@ -18,7 +18,7 @@ use crate::rpc::eth::{ }; use crate::rpc::prelude::*; use crate::shim::address::Address; -use crate::utils::encoding::{hex, keccak_256}; +use crate::utils::encoding::keccak_256; use anyhow::{Context as _, ensure}; use jsonrpsee::core::ClientError; use libtest_mimic::{Arguments, Failed, Trial}; @@ -99,14 +99,9 @@ fn recurse_calldata(depth: u64) -> Vec { out } -/// Deployed `NestedGas` ETH address for JSON-RPC and `forest-wallet send`. -struct Deployed { - eth: EthAddress, -} - /// Deploys `NestedGas` once per process. -async fn contract() -> anyhow::Result<&'static Deployed> { - static CONTRACT: OnceCell = OnceCell::const_new(); +async fn contract() -> anyhow::Result<&'static EthAddress> { + static CONTRACT: OnceCell = OnceCell::const_new(); CONTRACT .get_or_try_init(|| async { let from = sender().await?; @@ -116,9 +111,7 @@ async fn contract() -> anyhow::Result<&'static Deployed> { poll_until_actor_on("forest", f4, forest_client).await?; poll_until_actor_on("lotus", f4, lotus_client).await?; poll_until_next_epoch().await?; - anyhow::Ok(Deployed { - eth: EthAddress::from_filecoin_address(&f4)?, - }) + EthAddress::from_filecoin_address(&f4) }) .await } @@ -141,7 +134,7 @@ async fn sender() -> anyhow::Result<&'static str> { let parsed = Address::from_str(&addr).context("parsing the sender address")?; poll_until_actor_on("lotus", parsed, lotus_client).await?; import_lotus_wallet_into_forest(&addr)?; - Ok(addr) + anyhow::Ok(addr) }) .await? .as_str()) @@ -152,11 +145,11 @@ async fn estimate( calldata: Vec, block: BlockNumberOrHash, ) -> anyhow::Result { - let (from, deployed) = tokio::try_join!(sender(), contract())?; + let (from, to) = tokio::try_join!(sender(), contract())?; let from = Address::from_str(from).context("parsing the sender address")?; let msg = EthCallMessage { from: Some(EthAddress::from_filecoin_address(&from)?), - to: Some(deployed.eth), + to: Some(*to), data: Some(EthBytes(calldata)), ..Default::default() }; @@ -192,7 +185,7 @@ async fn poll_until_next_epoch() -> anyhow::Result<()> { /// height only after the deploy/fund guarantees the pinned tipset already contains the contract and /// sender on both nodes (the funding poll also lets both catch up to the deploy). async fn pinned_common_block() -> anyhow::Result<(Client, Client, i64)> { - tokio::try_join!(contract(), sender())?; + contract().await?; let (forest_c, lotus_c) = (forest_client()?, lotus_client()?); let block = common_block_number(&forest_c, &lotus_c).await?; Ok((forest_c, lotus_c, block)) @@ -241,17 +234,21 @@ async fn estimate_is_sufficient_on_chain() -> anyhow::Result<()> { ) .await?; let from = sender().await?; - let target = hex::encode_prefixed(contract().await?.eth.0.as_bytes()); // `forest-wallet send` infers `InvokeContract` and CBOR-wraps the params when the sender is an // eth account, and rejects an explicit `--method`, so pass the bare calldata. - let cid = wallet_send_calldata(from, &target, &recurse_calldata(NESTED_DEPTH), estimate) - .await - .with_context(|| { - format!( - "a transaction submitted at forest's own eth_estimateGas value ({estimate}) failed \ + let cid = wallet_send_calldata( + from, + contract().await?, + &recurse_calldata(NESTED_DEPTH), + estimate, + ) + .await + .with_context(|| { + format!( + "a transaction submitted at forest's own eth_estimateGas value ({estimate}) failed \ on chain; the estimate is not a usable gas limit" - ) - })?; + ) + })?; eprintln!("submitted at forest's estimate {estimate}: {cid}"); Ok(()) } diff --git a/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs index a3489322cf1d..be67a25817b2 100644 --- a/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs +++ b/src/dev/subcommands/devnet_cmd/eth_skip_sender.rs @@ -192,13 +192,6 @@ fn latest() -> BlockNumberOrHash { BlockNumberOrHash::PredefinedBlock(Predefined::Latest) } -/// Deployed EVM actor: `eth` for JSON-RPC and `forest-wallet send`, `f4` for `StateGetActor`. -#[derive(Clone, Copy)] -struct Deployed { - eth: EthAddress, - f4: Address, -} - /// Lotus `wallet new` string (`t4…`) plus parsed Filecoin and ETH forms. struct Wallet { cli: String, @@ -220,7 +213,7 @@ async fn deployer() -> anyhow::Result<&'static Address> { .await } -async fn deploy_hex(label: &str, bytecode: &str) -> anyhow::Result { +async fn deploy_hex(label: &str, bytecode: &str) -> anyhow::Result { let deployer = deployer().await?; let from = deployer.to_string(); let deploy = forest_evm_deploy_hex(&from, bytecode)?; @@ -228,14 +221,11 @@ async fn deploy_hex(label: &str, bytecode: &str) -> anyhow::Result { eprintln!("deployed {label} at {f4}"); poll_until_actor(f4).await?; poll_until_actor_on("lotus", f4, lotus_client).await?; - Ok(Deployed { - eth: EthAddress::from_filecoin_address(&f4)?, - f4, - }) + EthAddress::from_filecoin_address(&f4) } -async fn simple_coin() -> anyhow::Result<&'static Deployed> { - static CONTRACT: OnceCell = OnceCell::const_new(); +async fn simple_coin() -> anyhow::Result<&'static EthAddress> { + static CONTRACT: OnceCell = OnceCell::const_new(); CONTRACT .get_or_try_init(|| deploy_hex("SimpleCoin", SIMPLE_COIN_HEX)) .await @@ -245,7 +235,7 @@ async fn evm_deploy_and_call() -> anyhow::Result<()> { let coin = simple_coin().await?; let from = EthAddress::from_filecoin_address(deployer().await?)?; let from_hex = hex::encode_prefixed(from.0.as_bytes()); - let to_hex = hex::encode_prefixed(coin.eth.0.as_bytes()); + let to_hex = hex::encode_prefixed(coin.0.as_bytes()); let data = hex::encode_prefixed(get_balance_calldata(from)); let out = forest_cli(&["evm", "call", &from_hex, &to_hex, &data])?; let result = out @@ -265,22 +255,22 @@ async fn evm_deploy_and_call() -> anyhow::Result<()> { Ok(()) } -async fn contract_b() -> anyhow::Result<&'static Deployed> { - static CONTRACT: OnceCell = OnceCell::const_new(); +async fn contract_b() -> anyhow::Result<&'static EthAddress> { + static CONTRACT: OnceCell = OnceCell::const_new(); CONTRACT .get_or_try_init(|| deploy_hex("ContractB", CONTRACT_B_HEX)) .await } -async fn nested_gas() -> anyhow::Result<&'static Deployed> { - static CONTRACT: OnceCell = OnceCell::const_new(); +async fn nested_gas() -> anyhow::Result<&'static EthAddress> { + static CONTRACT: OnceCell = OnceCell::const_new(); CONTRACT .get_or_try_init(|| deploy_hex("NestedGas", NESTED_GAS_HEX)) .await } -async fn errors_contract() -> anyhow::Result<&'static Deployed> { - static CONTRACT: OnceCell = OnceCell::const_new(); +async fn errors_contract() -> anyhow::Result<&'static EthAddress> { + static CONTRACT: OnceCell = OnceCell::const_new(); CONTRACT .get_or_try_init(|| deploy_hex("Errors", ERRORS_HEX)) .await @@ -302,8 +292,8 @@ async fn table_env() -> anyhow::Result<&'static TableEnv> { let eoa = new_funded(EOA_FUND_AMT).await?; let eoa2 = new_unfunded().await?; Ok(TableEnv { - coin: coin.eth, - errors: errors.eth, + coin: *coin, + errors: *errors, eoa: eoa.eth, eoa2: eoa2.eth, }) @@ -312,13 +302,13 @@ async fn table_env() -> anyhow::Result<&'static TableEnv> { } /// `ContractA` with `setContractB` already mined, so callbacks see `storedValue`. -async fn linked_contracts() -> anyhow::Result<&'static (Deployed, Deployed)> { - static LINKED: OnceCell<(Deployed, Deployed)> = OnceCell::const_new(); +async fn linked_contracts() -> anyhow::Result<&'static (EthAddress, EthAddress)> { + static LINKED: OnceCell<(EthAddress, EthAddress)> = OnceCell::const_new(); LINKED .get_or_try_init(|| async { let b = contract_b().await?; let a = deploy_hex("ContractA", CONTRACT_A_HEX).await?; - invoke(&a.f4, &set_contract_b_calldata(b.eth)).await?; + invoke(a, &set_contract_b_calldata(*b)).await?; Ok((a, *b)) }) .await @@ -347,9 +337,10 @@ async fn fund_on_chain(cli_addr: &str, amount: &str) -> anyhow::Result
Ok(addr) } -async fn invoke(to: &Address, calldata: &[u8]) -> anyhow::Result<()> { +async fn invoke(to: EthAddress, calldata: &[u8]) -> anyhow::Result<()> { let from = deployer().await?.to_string(); - forest_evm_invoke(&from, &to.to_string(), &hex::encode(calldata))?; + let to_fil = to.to_filecoin_address()?.to_string(); + forest_evm_invoke(&from, &to_fil, &hex::encode(calldata))?; Ok(()) } @@ -835,7 +826,7 @@ async fn round_trip_from_unfunded() -> anyhow::Result<()> { let calldata = send_coin_calldata(recipient, 0); let from = new_unfunded().await?; - let gas = estimate_gas(&forest, from.eth, coin.eth, calldata.clone()) + let gas = estimate_gas(&forest, from.eth, *coin, calldata.clone()) .await .context("eth_estimateGas from unfunded sender")?; eprintln!("skip-sender estimate {gas} from {}", from.cli); @@ -855,14 +846,9 @@ async fn round_trip_from_unfunded() -> anyhow::Result<()> { actor.sequence ); - let cid = wallet_send_calldata( - &from.cli, - &hex::encode_prefixed(coin.eth.0.as_bytes()), - &calldata, - gas, - ) - .await - .with_context(|| format!("transaction submitted at eth_estimateGas {gas} failed"))?; + let cid = wallet_send_calldata(&from.cli, coin, &calldata, gas) + .await + .with_context(|| format!("transaction submitted at eth_estimateGas {gas} failed"))?; eprintln!("submitted at estimate {gas}: {cid}"); let after = get_actor(&forest, from.f4) .await? @@ -880,11 +866,11 @@ async fn parity_with_existing_sender() -> anyhow::Result<()> { let coin = simple_coin().await?; let calldata = send_coin_calldata(non_existent(0x01)?, 0); - let skip = estimate_gas(&forest, non_existent(0x42)?, coin.eth, calldata.clone()) + let skip = estimate_gas(&forest, non_existent(0x42)?, *coin, calldata.clone()) .await .context("eth_estimateGas from missing from")?; let placeholder = new_funded(ROUND_TRIP_FUND_AMT).await?; - let funded = estimate_gas(&forest, placeholder.eth, coin.eth, calldata) + let funded = estimate_gas(&forest, placeholder.eth, *coin, calldata) .await .context("eth_estimateGas from funded placeholder")?; eprintln!("parity skip={skip} funded={funded}"); @@ -901,12 +887,12 @@ async fn round_trip_recursive() -> anyhow::Result<()> { let calldata = recurse_calldata(NESTED_DEPTH); let from = new_unfunded().await?; - let gas = estimate_gas(&forest, from.eth, nested.eth, calldata.clone()) + let gas = estimate_gas(&forest, from.eth, *nested, calldata.clone()) .await .context("skip-sender eth_estimateGas recurse(100)")?; let placeholder = new_funded(RECURSIVE_FUND_AMT).await?; - let funded = estimate_gas(&forest, placeholder.eth, nested.eth, calldata.clone()) + let funded = estimate_gas(&forest, placeholder.eth, *nested, calldata.clone()) .await .context("funded-placeholder eth_estimateGas recurse(100)")?; eprintln!("recursive skip={gas} funded={funded}"); @@ -916,14 +902,9 @@ async fn round_trip_recursive() -> anyhow::Result<()> { ); fund_on_chain(&from.cli, RECURSIVE_FUND_AMT).await?; - wallet_send_calldata( - &from.cli, - &hex::encode_prefixed(nested.eth.0.as_bytes()), - &calldata, - gas, - ) - .await - .with_context(|| format!("transaction submitted at eth_estimateGas {gas} failed"))?; + wallet_send_calldata(&from.cli, nested, &calldata, gas) + .await + .with_context(|| format!("transaction submitted at eth_estimateGas {gas} failed"))?; Ok(()) } @@ -935,17 +916,17 @@ async fn call_sender_identity() -> anyhow::Result<()> { let without_coins = non_existent(0x22)?; let recipient = non_existent(0x01)?; - for to in [sender_contract.eth, with_coins] { - invoke(&coin.f4, &send_coin_calldata(to, 100)).await?; + for to in [*sender_contract, with_coins] { + invoke(*coin, &send_coin_calldata(to, 100)).await?; } let spend = send_coin_calldata(recipient, 10); for (label, from, want) in [ - ("contract from", sender_contract.eth, 1u8), + ("contract from", *sender_contract, 1u8), ("credited missing from", with_coins, 1), ("uncounted missing from", without_coins, 0), ] { - let ret = eth_call(&forest, from, coin.eth, spend.clone(), latest()) + let ret = eth_call(&forest, from, *coin, spend.clone(), latest()) .await .with_context(|| format!("eth_call sendCoin from {label}"))?; ensure!( @@ -1025,7 +1006,7 @@ async fn assert_call_b( let forest = forest_client()?; let (a, _) = linked_contracts().await?; assert_abi_u256( - eth_call(&forest, from, a.eth, selector(sig), latest()) + eth_call(&forest, from, *a, selector(sig), latest()) .await .with_context(|| label.to_string())?, expected, @@ -1036,7 +1017,7 @@ async fn assert_call_b( async fn cross_contract_from_contract() -> anyhow::Result<()> { let (_, b) = linked_contracts().await?; assert_call_b( - b.eth, + *b, CALL_B_AND_READ_BACK, 42, "cross-contract callback from contract from", diff --git a/src/dev/subcommands/tests_cmd/helpers.rs b/src/dev/subcommands/tests_cmd/helpers.rs index 50e573eb6a3f..8b84387a3d04 100644 --- a/src/dev/subcommands/tests_cmd/helpers.rs +++ b/src/dev/subcommands/tests_cmd/helpers.rs @@ -15,6 +15,7 @@ use serde_json::{Value, json}; use tempfile::NamedTempFile; use tokio::sync::OnceCell; +use crate::rpc::eth::types::EthAddress; use crate::rpc::prelude::*; use crate::rpc::types::{ApiTipsetKey, MessageLookup}; use crate::rpc::{Client, humanize_rpc_error}; @@ -177,19 +178,20 @@ pub async fn assert_send_ok(out: &str) -> anyhow::Result { /// Imports `from` from the Lotus keystore into Forest's remote wallet when needed. pub async fn wallet_send_calldata( from: &str, - to: &str, + to: &EthAddress, calldata: &[u8], gas_limit: u64, ) -> anyhow::Result { if wallet(Backend::Remote, &["has", from])? != "true" { import_lotus_wallet_into_forest(from)?; } + let to_hex = hex::encode_prefixed(to.0.as_bytes()); let params = hex::encode(calldata); let gas = gas_limit.to_string(); let out = wallet_send( Backend::Remote, from, - to, + &to_hex, "0", &["--params-hex", params.as_str(), "--gas-limit", gas.as_str()], true, diff --git a/src/wallet/subcommands/wallet_cmd.rs b/src/wallet/subcommands/wallet_cmd.rs index 9087717d0c4b..e5b88b4d44bc 100644 --- a/src/wallet/subcommands/wallet_cmd.rs +++ b/src/wallet/subcommands/wallet_cmd.rs @@ -871,9 +871,7 @@ mod tests { assert_eq!(invocation.method_num, EVMMethod::InvokeContract as u64); assert_eq!( invocation.params.to_vec(), - crate::eth::get_filecoin_method_info(Some(ð), &calldata) - .unwrap() - .params + crate::eth::encode_evm_params(&calldata).unwrap() ); } From 017d81b579f9f148ebe5fadc013db01c735a57f1 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 11 Sep 2026 09:23:05 +0530 Subject: [PATCH 3/3] update comment --- docs/docs/users/reference/cli.md | 2 +- src/wallet/subcommands/wallet_cmd.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/users/reference/cli.md b/docs/docs/users/reference/cli.md index 035f3801624b..76ba2e3661ce 100644 --- a/docs/docs/users/reference/cli.md +++ b/docs/docs/users/reference/cli.md @@ -342,7 +342,7 @@ Options: --wait-timeout Timeout duration for `--wait-confidence`, e.g. `30s`, `5m`. If not set, the timeout will be `confidence + 5` epochs --method - Specify method to invoke (default: 0) + Specify method to invoke (default 0; selected automatically for ETH) --params-hex Specify invocation parameters in hex -h, --help diff --git a/src/wallet/subcommands/wallet_cmd.rs b/src/wallet/subcommands/wallet_cmd.rs index e5b88b4d44bc..df6da3293c03 100644 --- a/src/wallet/subcommands/wallet_cmd.rs +++ b/src/wallet/subcommands/wallet_cmd.rs @@ -322,7 +322,7 @@ pub enum WalletCommands { /// Timeout duration for `--wait-confidence`, e.g. `30s`, `5m`. If not set, the timeout will be `confidence + 5` epochs. #[arg(long, requires = "wait_confidence", value_parser = humantime::parse_duration)] wait_timeout: Option, - /// Specify method to invoke (default: 0) + /// Specify method to invoke (default 0; selected automatically for ETH) #[arg(long)] method: Option, /// Specify invocation parameters in hex