feat: forc-call inline abi support (#7270)
Some checks failed
CI / forc-fmt-check-panic (push) Has been cancelled
CI / forc-fmt-check-sway-lib-std (push) Has been cancelled
CI / forc-fmt-check-sway-examples (push) Has been cancelled
CI / cargo-fmt-check (push) Has been cancelled
Codspeed Benchmarks / benchmarks (push) Has been cancelled
CI / build-reference-examples (push) Has been cancelled
CI / check-dependency-version-formats (push) Has been cancelled
CI / check-forc-manifest-version (push) Has been cancelled
CI / get-fuel-core-version (push) Has been cancelled
CI / build-sway-lib-std (push) Has been cancelled
CI / build-sway-examples (push) Has been cancelled
CI / check-sdk-harness-test-suite-compatibility (push) Has been cancelled
CI / build-mdbook (push) Has been cancelled
CI / build-forc-doc-sway-lib-std (push) Has been cancelled
CI / build-forc-test-project (push) Has been cancelled
CI / cargo-build-workspace (push) Has been cancelled
CI / cargo-clippy (push) Has been cancelled
CI / cargo-toml-fmt-check (push) Has been cancelled
CI / cargo-run-e2e-test-evm (push) Has been cancelled
CI / cargo-test-lib-std (push) Has been cancelled
CI / forc-run-benchmarks (push) Has been cancelled
CI / forc-unit-tests (push) Has been cancelled
CI / forc-pkg-fuels-deps-check (push) Has been cancelled
CI / cargo-test-sway-lsp (push) Has been cancelled
CI / cargo-test-forc (push) Has been cancelled
CI / cargo-test-workspace (push) Has been cancelled
CI / cargo-unused-deps-check (push) Has been cancelled
CI / pre-publish-check (push) Has been cancelled
github pages / deploy (push) Has been cancelled
CI / verifications-complete (push) Has been cancelled
CI / cargo-run-e2e-test (push) Has been cancelled
CI / cargo-run-e2e-test-release (push) Has been cancelled
CI / cargo-test-forc-debug (push) Has been cancelled
CI / cargo-test-forc-client (push) Has been cancelled
CI / cargo-test-forc-node (push) Has been cancelled
CI / notify-slack-on-failure (push) Has been cancelled
CI / publish (push) Has been cancelled
CI / publish-sway-lib-std (push) Has been cancelled
CI / Build and upload forc binaries to release (push) Has been cancelled

## Description

This pull request introduces a enhancement to the ABI handling in the
`forc-client` plugin by replacing the `Either<PathBuf, Url>` type with a
new `AbiSource` enum.
This improves flexibility and usability by supporting ABI sources as
file paths, URLs, or raw JSON strings.
Additionally, it simplifies related code and updates documentation and
tests accordingly.

This represents an additional optional for callers to use forc-call with
an ABI available at-hand without needing to write to a file first.

## Checklist

- [ ] I have linked to any relevant issues.
- [ ] I have commented my code, particularly in hard-to-understand
areas.
- [ ] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [ ] If my change requires substantial documentation changes, I have
[requested support from the DevRel
team](https://github.com/FuelLabs/devrel-requests/issues/new/choose)
- [ ] I have added tests that prove my fix is effective or that my
feature works.
- [ ] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [ ] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [ ] I have requested a review from the relevant team or maintainers.

---------

Co-authored-by: z <zees-dev@users.noreply.github.com>
This commit is contained in:
zees-dev 2025-07-04 20:01:13 +12:00 committed by GitHub
parent de2a5ac0f0
commit b54fce7727
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 145 additions and 72 deletions

View file

@ -1,6 +1,5 @@
use crate::NodeTarget;
use clap::{Parser, ValueEnum};
use either::Either;
use fuel_crypto::SecretKey;
use fuels::programs::calls::CallParameters;
use fuels_core::types::{Address, AssetId, ContractId};
@ -138,19 +137,64 @@ impl From<CallParametersOpts> for CallParameters {
}
/// Operation for the call command
#[derive(Debug, Clone, PartialEq)]
pub enum AbiSource {
/// ABI from file path
File(PathBuf),
/// ABI from URL
Url(Url),
/// ABI as raw string
String(String),
}
impl std::fmt::Display for AbiSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AbiSource::File(path) => write!(f, "{}", path.display()),
AbiSource::Url(url) => write!(f, "{}", url),
AbiSource::String(s) => write!(f, "{}", s),
}
}
}
impl TryFrom<String> for AbiSource {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
// First try to parse as URL
if let Ok(url) = Url::parse(&s) {
match url.scheme() {
"http" | "https" | "ipfs" => return Ok(AbiSource::Url(url)),
_ => {} // Continue to check other options
}
}
// Check if it looks like a JSON string (starts with '{' or '[')
let trimmed = s.trim();
if (trimmed.starts_with('{') && trimmed.ends_with('}'))
|| (trimmed.starts_with('[') && trimmed.ends_with(']'))
{
return Ok(AbiSource::String(s));
}
// Default to treating as file path
Ok(AbiSource::File(PathBuf::from(s)))
}
}
#[derive(Debug, Clone)]
pub enum Operation {
/// Call a specific contract function
CallFunction {
contract_id: ContractId,
abi: Either<PathBuf, Url>,
abi: AbiSource,
function: FuncType,
function_args: Vec<String>,
},
/// List all functions in the contract
ListFunctions {
contract_id: ContractId,
abi: Either<PathBuf, Url>,
abi: AbiSource,
},
/// Direct transfer of assets to a contract
DirectTransfer {
@ -269,6 +313,13 @@ forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \
--list-functions
```
### Call a contract with inline ABI JSON string
```sh
forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \
--abi '{"functions":[{"inputs":[],"name":"get_balance","output":{"name":"","type":"u64","typeArguments":null}}]}' \
get_balance
```
### Direct transfer of asset to a contract or address
```sh
forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \
@ -281,17 +332,18 @@ pub struct Command {
#[clap(help_heading = "CONTRACT")]
pub address: Address,
/// Path or URI to a JSON ABI file
/// Path, URI, or raw JSON string for the ABI
/// Required when making function calls or listing functions
#[clap(long, value_parser = parse_abi_path)]
pub abi: Option<Either<PathBuf, Url>>,
/// Can be a file path, HTTP/HTTPS URL, or raw JSON string
#[clap(long, value_parser = |s: &str| AbiSource::try_from(s.to_string()))]
pub abi: Option<AbiSource>,
/// Additional contract IDs and their ABI paths for better tracing and debugging.
/// Format: contract_id:abi_path (can be used multiple times)
/// Example: --contract-abi 0x123:./abi1.json --contract-abi 0x456:https://example.com/abi2.json
/// Contract IDs can be provided with or without 0x prefix
#[clap(long = "contract-abi", value_parser = parse_contract_abi, action = clap::ArgAction::Append, help_heading = "CONTRACT")]
pub contract_abis: Option<Vec<(ContractId, Either<PathBuf, Url>)>>,
pub contract_abis: Option<Vec<(ContractId, AbiSource)>>,
/// Label addresses in the trace output for better readability.
/// Format: address:label (can be used multiple times)
@ -404,18 +456,7 @@ impl Command {
}
}
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)))
}
}
fn parse_contract_abi(s: &str) -> Result<(ContractId, Either<PathBuf, Url>), String> {
fn parse_contract_abi(s: &str) -> Result<(ContractId, AbiSource), String> {
let parts: Vec<&str> = s.trim().split(':').collect();
let [contract_id_str, abi_path_str] = parts.try_into().map_err(|_| {
format!(
@ -428,7 +469,7 @@ fn parse_contract_abi(s: &str) -> Result<(ContractId, Either<PathBuf, Url>), Str
ContractId::from_str(&format!("0x{}", contract_id_str.trim_start_matches("0x")))
.map_err(|e| format!("Invalid contract ID '{}': {}", contract_id_str, e))?;
let abi_path = parse_abi_path(abi_path_str)
let abi_path = AbiSource::try_from(abi_path_str.to_string())
.map_err(|e| format!("Invalid ABI path '{}': {}", abi_path_str, e))?;
Ok((contract_id, abi_path))
@ -449,3 +490,26 @@ fn parse_label(s: &str) -> Result<(ContractId, String), String> {
Ok((contract_id, label.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_abi_source_try_from() {
let url_result = AbiSource::try_from("https://example.com/abi.json".to_string()).unwrap();
assert!(matches!(url_result, AbiSource::Url(_)));
let json_result = AbiSource::try_from(r#"{"functions": []}"#.to_string()).unwrap();
assert!(matches!(json_result, AbiSource::String(_)));
let array_result = AbiSource::try_from("[]".to_string()).unwrap();
assert!(matches!(array_result, AbiSource::String(_)));
let file_result = AbiSource::try_from("./contract-abi.json".to_string()).unwrap();
assert!(matches!(file_result, AbiSource::File(_)));
let file_url_result = AbiSource::try_from("file:///path/to/abi.json".to_string()).unwrap();
assert!(matches!(file_url_result, AbiSource::File(_)));
}
}

View file

@ -4,7 +4,7 @@ use crate::{
missing_contracts::determine_missing_contracts,
parser::{param_type_val_to_token, token_to_string},
trace::interpret_execution_trace,
CallResponse, Either,
CallResponse,
},
};
use anyhow::{anyhow, bail, Result};
@ -32,13 +32,12 @@ use fuels_core::{
ContractId,
},
};
use std::{collections::HashMap, path::PathBuf};
use url::Url;
use std::collections::HashMap;
/// Calls a contract function with the given parameters
pub async fn call_function(
contract_id: ContractId,
abi: Either<PathBuf, Url>,
abi: crate::cmd::call::AbiSource,
function: FuncType,
function_args: Vec<String>,
cmd: cmd::Call,
@ -397,6 +396,7 @@ pub mod tests {
op::call::{call, get_wallet, PrivateKeySigner},
};
use fuels::{crypto::SecretKey, prelude::*};
use std::path::PathBuf;
fn get_contract_call_cmd(
id: ContractId,
@ -407,7 +407,7 @@ pub mod tests {
) -> cmd::Call {
cmd::Call {
address: (*id).into(),
abi: Some(Either::Left(std::path::PathBuf::from(
abi: Some(cmd::call::AbiSource::File(PathBuf::from(
"../../forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-abi.json",
))),
function: Some(selector.to_string()),

View file

@ -1,6 +1,11 @@
use crate::op::call::{
parser::{get_default_value, param_to_function_arg, param_type_val_to_token, token_to_string},
Abi,
use crate::{
cmd::call::AbiSource,
op::call::{
parser::{
get_default_value, param_to_function_arg, param_type_val_to_token, token_to_string,
},
Abi,
},
};
use anyhow::{anyhow, Result};
use fuels_core::types::{param_types::ParamType, ContractId};
@ -17,12 +22,7 @@ pub fn list_contract_functions<W: Write>(
) -> Result<()> {
// First, list functions for the main contract
if let Some(main_abi) = abi_map.get(main_contract_id) {
list_functions_for_single_contract(
main_contract_id,
main_abi,
true, // is_main_contract
writer,
)?;
list_functions_for_single_contract(main_contract_id, main_abi, true, writer)?;
} else {
return Err(anyhow!("Main contract ABI not found in abi_map"));
}
@ -38,12 +38,7 @@ pub fn list_contract_functions<W: Write>(
writeln!(writer, "Additional Contracts:\n")?;
for (contract_id, abi) in additional_contracts {
list_functions_for_single_contract(
contract_id,
abi,
false, // is_main_contract
writer,
)?;
list_functions_for_single_contract(contract_id, abi, false, writer)?;
}
}
@ -133,20 +128,29 @@ fn list_functions_for_single_contract<W: Write>(
)
})?;
// Since we don't know the original ABI path, we'll use a placeholder
let abi_placeholder = "./contract-abi.json";
let painted_name = forc_util::ansiterm::Colour::Blue.paint(func.name.clone());
writeln!(
writer,
"{}({}) -> {}",
painted_name, func_args_types, return_type
)?;
writeln!(
writer,
" forc call \\\n --abi {} \\\n {} \\\n {} {}\n",
abi_placeholder, contract_id, func.name, func_args_inputs,
)?;
match &abi.source {
AbiSource::String(s) => {
// json string in quotes for shell
writeln!(
writer,
" forc call \\\n --abi \"{}\" \\\n {} \\\n {} {}\n",
s, contract_id, func.name, func_args_inputs,
)?;
}
_ => {
writeln!(
writer,
" forc call \\\n --abi {} \\\n {} \\\n {} {}\n",
abi.source, contract_id, func.name, func_args_inputs,
)?;
}
}
}
Ok(())
@ -184,24 +188,21 @@ mod tests {
let output_bytes = output.into_inner();
let output_string = String::from_utf8(output_bytes).expect("Output was not valid UTF-8");
// Verify the output contains expected function names and formatting
// Check that the output contains key elements instead of exact string match
assert!(output_string.contains("Callable functions for contract:"));
assert!(output_string.contains(
"\u{1b}[34mtest_struct_with_generic\u{1b}[0m(a: GenericStruct) -> GenericStruct"
));
assert!(output_string.contains("forc call \\"));
assert!(output_string.contains("--abi ./contract-abi.json \\"));
assert!(output_string.contains(format!("{id} \\").as_str()));
assert!(output_string.contains("test_struct_with_generic \"{0, aaaa}\""));
assert!(output_string
.contains("\u{1b}[34mtest_complex_struct\u{1b}[0m(a: ComplexStruct) -> ComplexStruct"));
assert!(output_string.contains("forc call \\"));
assert!(output_string.contains("--abi ./contract-abi.json \\"));
assert!(output_string.contains(format!("{id} \\").as_str()));
assert!(output_string.contains(
"test_complex_struct \"{({aa, 0}, 0), (Active:false), 0, {{0, aaaa}, aaaa}}\""
));
.contains("053efe51968252f029899660d7064124084a48136e326e467f62cb7f5913ba77"));
assert!(output_string.contains("forc call"));
assert!(output_string.contains("programType"));
assert!(output_string.contains("contract"));
assert!(output_string.contains("functions"));
// Verify that we have some function names
assert!(output_string.contains("test_"));
assert!(output_string.contains("transfer"));
// Verify ABI structure is present
assert!(output_string.contains("concreteTypes"));
assert!(output_string.contains("metadataTypes"));
}
}

View file

@ -5,6 +5,7 @@ mod parser;
mod trace;
mod transfer;
use crate::cmd::call::AbiSource;
use crate::{
cmd,
constants::DEFAULT_PRIVATE_KEY,
@ -15,7 +16,6 @@ use crate::{
util::tx::{prompt_forc_wallet_password, select_local_wallet_account},
};
use anyhow::{anyhow, Result};
use either::Either;
use fuel_abi_types::abi::{
program::ProgramABI,
unified_program::{UnifiedProgramABI, UnifiedTypeDeclaration},
@ -126,12 +126,12 @@ async fn setup_connection(
Ok((wallet, tx_policies, *base_asset_id))
}
/// Helper function to load ABI from file or URL
async fn load_abi(abi: &Either<std::path::PathBuf, url::Url>) -> anyhow::Result<String> {
/// Helper function to load ABI from file, URL, or raw string
async fn load_abi(abi: &AbiSource) -> anyhow::Result<String> {
match abi {
Either::Left(path) => std::fs::read_to_string(path)
AbiSource::File(path) => std::fs::read_to_string(path)
.map_err(|e| anyhow!("Failed to read ABI file at {:?}: {}", path, e)),
Either::Right(url) => {
AbiSource::Url(url) => {
let response = reqwest::get(url.clone())
.await
.map_err(|e| anyhow!("Failed to fetch ABI from URL {}: {}", url, e))?;
@ -142,6 +142,12 @@ async fn load_abi(abi: &Either<std::path::PathBuf, url::Url>) -> anyhow::Result<
String::from_utf8(bytes.to_vec())
.map_err(|e| anyhow!("Failed to parse response as UTF-8 from URL {}: {}", url, e))
}
AbiSource::String(json_str) => {
// Validate that it's valid JSON
serde_json::from_str::<serde_json::Value>(json_str)
.map_err(|e| anyhow!("Invalid JSON in ABI string: {}", e))?;
Ok(json_str.to_owned())
}
}
}
@ -189,6 +195,7 @@ async fn get_wallet(
#[derive(Debug, Clone)]
pub(crate) struct Abi {
source: AbiSource,
program: ProgramABI,
unified: UnifiedProgramABI,
// TODO: required for vm interpreter step through
@ -213,6 +220,7 @@ impl FromStr for Abi {
.collect::<HashMap<_, _>>();
Ok(Self {
source: AbiSource::String(s.to_string()),
program,
unified,
type_lookup,
@ -310,8 +318,8 @@ pub(crate) fn display_detailed_call_info(
/// This is a reusable function for both call_function and list_functions operations
pub(crate) async fn create_abi_map(
main_contract_id: ContractId,
main_abi: &Either<std::path::PathBuf, url::Url>,
additional_contract_abis: Option<Vec<(ContractId, Either<std::path::PathBuf, url::Url>)>>,
main_abi: &AbiSource,
additional_contract_abis: Option<Vec<(ContractId, AbiSource)>>,
) -> anyhow::Result<HashMap<ContractId, Abi>> {
// Load main ABI
let main_abi_str = load_abi(main_abi).await?;