Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugin: forc-call #6791

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
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
28 changes: 18 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion forc-plugins/forc-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ chrono = { workspace = true, features = ["std"] }
clap = { workspace = true, features = ["derive", "env"] }
devault.workspace = true
dialoguer.workspace = true
either.workspace = true
forc.workspace = true
forc-pkg.workspace = true
forc-tracing.workspace = true
Expand All @@ -36,26 +37,32 @@ futures.workspace = true
hex.workspace = true
k256.workspace = true
rand.workspace = true
regex.workspace = true
reqwest = { workspace = true }
rpassword.workspace = true
serde.workspace = true
serde_json.workspace = true
sway-ast.workspace = true
sway-core.workspace = true
sway-features.workspace = true
sway-parse.workspace = true
sway-types.workspace = true
sway-utils.workspace = true
swayfmt.workspace = true
tempfile.workspace = true
tokio = { workspace = true, features = [
"macros",
"process",
"rt-multi-thread",
] }
toml_edit.workspace = true
tracing.workspace = true
url.workspace = true

[dev-dependencies]
portpicker = "0.1.1"
pretty_assertions = "1.4.1"
rexpect = "0.5"
tempfile = "3"

[build-dependencies]
regex = "1.5.4"
Expand Down
12 changes: 12 additions & 0 deletions forc-plugins/forc-client/src/bin/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use clap::Parser;
use forc_tracing::{init_tracing_subscriber, println_error};

#[tokio::main]
async fn main() {
init_tracing_subscriber(Default::default());
let command = forc_client::cmd::Call::parse();
if let Err(err) = forc_client::op::call(command).await {
println_error(&format!("{}", err));
std::process::exit(1);
}
}
141 changes: 141 additions & 0 deletions forc-plugins/forc-client/src/cmd/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use crate::NodeTarget;
use clap::Parser;
use either::Either;
use fuel_crypto::SecretKey;
use fuels_core::types::ContractId;
use std::{path::PathBuf, str::FromStr};
use url::Url;

pub use forc::cli::shared::{BuildOutput, BuildProfile, Minify, Pkg, Print};
pub use forc_tx::{Gas, Maturity};

forc_util::cli_examples! {
super::Command {
[ Call a contract with function parameters => "forc call <CONTRACT_ID> <FUNCTION_SIGNATURE> <ARGS>" ]
[ Call a contract without function parameters => "forc call <CONTRACT_ID> <FUNCTION_SIGNATURE>" ]
[ Call a contract given an ABI file with function parameters => "forc call <CONTRACT_ID> --abi <ABI_FILE> <FUNCTION_SELECTOR> <ARGS>" ]
[ Call a contract that makes external contract calls => "forc call <CONTRACT_ID> --abi <ABI_FILE> <FUNCTION_SELECTOR> <ARGS> --contracts <CONTRACT_ADDRESS_1> <CONTRACT_ADDRESS_2>..." ]
[ Call a contract in simulation mode => "forc call <CONTRACT_ID> <FUNCTION_SIGNATURE> --simulate" ]
[ Call a contract in live mode which performs state changes => "forc call <CONTRACT_ID> <FUNCTION_SIGNATURE> --live" ]
}
}

#[derive(Debug, Clone)]
pub enum FuncType {
Signature(String),
Selector(String),
}

impl Default for FuncType {
fn default() -> Self {
FuncType::Signature(String::new())
}
}

impl FromStr for FuncType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim().replace(' ', "");
if s.is_empty() {
return Err("Function signature cannot be empty".to_string());
}
// Check if function signature is a valid selector (alphanumeric and underscore support)
let selector_pattern = regex::Regex::new(r"^[a-zA-Z][a-zA-Z0-9_]*$").unwrap();
if !selector_pattern.is_match(&s) {
return Ok(FuncType::Signature(s.to_string()));
}
Ok(FuncType::Selector(s.to_string()))
}
}

/// Flags for specifying the caller.
#[derive(Debug, Default, Parser, serde::Deserialize, serde::Serialize)]
pub struct Caller {
/// Derive an account from a secret key to make the call
#[clap(long, env = "SIGNING_KEY")]
pub signing_key: Option<SecretKey>,

/// Use forc-wallet to make the call
#[clap(long, default_value = "false")]
pub wallet: bool,
}

#[derive(Debug, Clone, PartialEq, Default)]
pub enum ExecutionMode {
/// Execute a dry run - no state changes, no gas fees, wallet is not used or validated
#[default]
DryRun,
/// Execute in simulation mode - no state changes, estimates gas, wallet is used but not validated
/// State changes are not applied
Simulate,
/// Execute live on chain - state changes, gas fees apply, wallet is used and validated
/// State changes are applied
Live,
}

impl FromStr for ExecutionMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"dry-run" => Ok(ExecutionMode::DryRun),
"simulate" => Ok(ExecutionMode::Simulate),
"live" => Ok(ExecutionMode::Live),
_ => Err(format!("Invalid execution mode: {}", s)),
}
}
}

/// Call a contract function.
#[derive(Debug, Default, Parser)]
#[clap(bin_name = "forc call", version)]
pub struct Command {
/// The contract ID to call.
pub contract_id: ContractId,

/// Optional path or URI to a JSON ABI file.
#[clap(long, value_parser = parse_abi_path)]
pub abi: Option<Either<PathBuf, Url>>,

/// The function signature to call.
/// When ABI is provided, this should be a selector (e.g. "transfer")
/// When no ABI is provided, this should be the full function signature (e.g. "transfer(address,u64)")
pub function: FuncType,

/// Arguments to pass into main function with forc run.
pub args: Vec<String>,

#[clap(flatten)]
pub gas: Option<Gas>,

#[clap(flatten)]
pub node: NodeTarget,

/// Select the caller to use for the call
#[clap(flatten)]
pub caller: Caller,

/// The execution mode to use for the call; defaults to dry-run; possible values: dry-run, simulate, live
#[clap(long, default_value = "dry-run")]
pub mode: ExecutionMode,

/// The gas price to use for the call; defaults to 0
#[clap(flatten)]
pub gas: Option<Gas>,

/// The external contract addresses to use for the call
/// If none are provided, the call will automatically extract contract addresses from the function arguments
/// and use them for the call as external contracts
#[clap(long, alias = "contracts")]
pub external_contracts: Option<Vec<ContractId>>,
}

fn parse_abi_path(s: &str) -> Result<Either<PathBuf, Url>, String> {
if let Ok(url) = Url::parse(s) {
match url.scheme() {
"http" | "https" | "ipfs" => Ok(Either::Right(url)),
_ => Err(format!("Unsupported URL scheme: {}", url.scheme())),
}
} else {
Ok(Either::Left(PathBuf::from(s)))
}
}
2 changes: 2 additions & 0 deletions forc-plugins/forc-client/src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
pub mod call;
pub mod deploy;
pub mod run;
pub mod submit;

pub use call::Command as Call;
pub use deploy::Command as Deploy;
pub use run::Command as Run;
pub use submit::Command as Submit;
Loading
Loading