Forc call transfers (#7056)

## Description

Implementation of direct transfer of assets to an address using
`forc-call` binary.
Refactor of the `forc-call` binary since it now supports 3 code paths:
- calling (query or tx submission) contracts
- transferring assets directly to an address (recipient and/or contract)
- listing contract functions with call examples

## Usage

```sh
# transfer default asset (eth) to `0x2c7Fd852EF2BaE281e90ccaDf18510701989469f7fc4b042F779b58a39919Eec`
forc-call 0x2c7Fd852EF2BaE281e90ccaDf18510701989469f7fc4b042F779b58a39919Eec --amount 2 --mode=live
```

### Output

```log
warning: No signing key or wallet flag provided. Using default signer: 0x6b63804cfbf9856e68e5b6e7aef238dc8311ec55bec04df774003a2c96e0418e

Transferring 2 0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07 to recipient address 0x2c7fd852ef2bae281e90ccadf18510701989469f7fc4b042f779b58a39919eec...

tx hash: e4e931276a500cce1b5303cb594c0477968124ab0f70e77995bbb4499e0c3350
```

## Additional notes

- Inspired by `cast send` where if the CLI is called without a function
signature, it transfers value (eth) to the target address based on the
`--value` parameter; `forc-call` does the same via the `--amount` param
- One can specify the `asset` to be sent; defaults to native network
asset (`eth`)
- Transfers only work with `--mode=live`; since there is no
simulation/dry-run functionality available for this
- Transfers to both contracts and user recipients is supported

## Checklist

- [ ] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [x] 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)
- [x] 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.
- [x] 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).
- [x] I have requested a review from the relevant team or maintainers.

---------

Co-authored-by: z <zees-dev@users.noreply.github.com>
Co-authored-by: kaya <kaya.gokalp@fuel.sh>
Co-authored-by: Joshua Batty <joshpbatty@gmail.com>
This commit is contained in:
zees-dev 2025-04-07 18:38:47 +12:00 committed by GitHub
parent 8e162c9661
commit d6410e37f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1373 additions and 879 deletions

View file

@ -50,6 +50,12 @@ forc call 0xe18de7c7c8c61a1c706dccb3533caa00ba5c11b5230da4428582abf1b6831b4d \
add 1 2
```
### Directly send funds to an address
```bash
forc call 0x2c7Fd852EF2BaE281e90ccaDf18510701989469f7fc4b042F779b58a39919Eec --amount 2 --mode=live
```
### Query the owner of a deployed [DEX contract](https://github.com/mira-amm/mira-v1-core) on testnet
```bash

View file

@ -5,7 +5,14 @@ use forc_tracing::{init_tracing_subscriber, println_error};
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 {
let operation = match command.validate_and_get_operation() {
Ok(operation) => operation,
Err(err) => {
println_error(&err);
std::process::exit(1);
}
};
if let Err(err) = forc_client::op::call(operation, command).await {
println_error(&format!("{}", err));
std::process::exit(1);
}

View file

@ -1,9 +1,9 @@
use crate::NodeTarget;
use clap::Parser;
use clap::{Parser, ValueEnum};
use either::Either;
use fuel_crypto::SecretKey;
use fuels::programs::calls::CallParameters;
use fuels_core::types::{AssetId, ContractId};
use fuels_core::types::{Address, AssetId, ContractId};
use std::{path::PathBuf, str::FromStr};
use url::Url;
@ -35,30 +35,60 @@ impl FromStr for FuncType {
}
}
/// Flags for specifying the caller.
/// Execution mode for contract calls
#[derive(Debug, Clone, PartialEq, Default, ValueEnum)]
#[clap(rename_all = "kebab-case")]
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
Simulate,
/// Execute live on chain - state changes, gas fees apply, wallet is used and validated
Live,
}
/// Output format for call results
#[derive(Debug, Clone, PartialEq, Default, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub enum OutputFormat {
/// Default formatted output
#[default]
Default,
/// Raw unformatted output
Raw,
}
/// Flags for specifying the caller account
#[derive(Debug, Default, Clone, Parser, serde::Deserialize, serde::Serialize)]
pub struct Caller {
/// Derive an account from a secret key to make the call
#[clap(long, env = "SIGNING_KEY")]
#[clap(long, env = "SIGNING_KEY", help_heading = "ACCOUNT OPTIONS")]
pub signing_key: Option<SecretKey>,
/// Use forc-wallet to make the call
#[clap(long, default_value = "false")]
#[clap(long, default_value = "false", help_heading = "ACCOUNT OPTIONS")]
pub wallet: bool,
}
/// Options for contract call parameters
#[derive(Debug, Default, Clone, Parser)]
pub struct CallParametersOpts {
/// Amount of native assets to forward with the call
#[clap(long, default_value = "0", alias = "value")]
#[clap(
long,
default_value = "0",
alias = "value",
help_heading = "CALL PARAMETERS"
)]
pub amount: u64,
/// Asset ID to forward with the call
#[clap(long)]
#[clap(long, help_heading = "CALL PARAMETERS")]
pub asset_id: Option<AssetId>,
/// Amount of gas to forward with the call
#[clap(long)]
#[clap(long, help_heading = "CALL PARAMETERS")]
pub gas_forwarded: Option<u64>,
}
@ -78,47 +108,27 @@ impl From<CallParametersOpts> for CallParameters {
}
}
#[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)),
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum OutputFormat {
#[default]
Default,
Raw,
}
impl FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"default" => Ok(OutputFormat::Default),
"raw" => Ok(OutputFormat::Raw),
_ => Err(format!("Invalid output format: {}", s)),
}
}
/// Operation for the call command
#[derive(Debug, Clone)]
pub enum Operation {
/// Call a specific contract function
CallFunction {
contract_id: ContractId,
abi: Either<PathBuf, Url>,
function: FuncType,
function_args: Vec<String>,
},
/// List all functions in the contract
ListFunctions {
contract_id: ContractId,
abi: Either<PathBuf, Url>,
},
/// Direct transfer of assets to a contract
DirectTransfer {
recipient: Address,
amount: u64,
asset_id: Option<AssetId>,
},
}
/// Perform Fuel RPC calls from the comfort of your command line.
@ -183,43 +193,58 @@ impl FromStr for OutputFormat {
» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \
--abi ./contract-abi.json \
--list-functions
# Direct transfer of asset to a contract or address
» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \
--amount 100 \
--mode live
"#)]
pub struct Command {
/// The contract ID to call.
pub contract_id: ContractId,
/// The contract ID to call
#[clap(help_heading = "CONTRACT")]
pub address: Address,
/// Path or URI to a JSON ABI file.
/// Path or URI to a JSON ABI file
/// Required when making function calls or listing functions
#[clap(long, value_parser = parse_abi_path)]
pub abi: Either<PathBuf, Url>,
pub abi: Option<Either<PathBuf, Url>>,
/// The function selector to call.
/// The function selector is the name of the function to call (e.g. "transfer").
/// It must be a valid selector present in the ABI file.
/// Not required when --list-functions is specified.
#[clap(required_unless_present = "list_functions")]
pub function: Option<FuncType>,
/// Not required when --list-functions is specified or when --amount is provided for direct transfer
#[clap(help_heading = "FUNCTION")]
pub function: Option<String>,
/// Arguments to pass into the function to be called.
/// Arguments to pass to the function
#[clap(help_heading = "FUNCTION")]
pub function_args: Vec<String>,
/// Network connection options
#[clap(flatten)]
pub node: NodeTarget,
/// Select the caller to use for the call
/// Caller account options
#[clap(flatten)]
pub caller: Caller,
/// Call parameters to use for the call
/// Call parameters
#[clap(flatten)]
pub call_parameters: CallParametersOpts,
/// The execution mode to use for the call; defaults to dry-run; possible values: dry-run, simulate, live
#[clap(long, default_value = "dry-run")]
/// Execution mode - determines if state changes are applied
/// - `dry-run`: No state changes, no gas fees, wallet is not used or validated
/// - `simulate`: No state changes, estimates gas, wallet is used but not validated
/// - `live`: State changes, gas fees apply, wallet is used and validated
#[clap(long, default_value = "dry-run", help_heading = "EXECUTION")]
pub mode: ExecutionMode,
/// List all available functions in the contract
/// Requires ABI and contract ID to be provided
#[clap(long, alias = "list-functions")]
#[clap(
long,
alias = "list-functions",
conflicts_with = "function",
help_heading = "OPERATION"
)]
pub list_functions: bool,
/// The gas price to use for the call; defaults to 0
@ -229,18 +254,63 @@ pub struct Command {
/// The external contract addresses to use for the call
/// If none are provided, the call will automatically populate external contracts by making a dry-run calls
/// to the node, and extract the contract addresses based on the revert reason
#[clap(long, alias = "contracts")]
#[clap(long, alias = "contracts", help_heading = "CONTRACT")]
pub external_contracts: Option<Vec<ContractId>>,
/// The output format to use; possible values: default, raw
#[clap(long, default_value = "default")]
/// Output format for the call result
#[clap(long, default_value = "default", help_heading = "OUTPUT")]
pub output: OutputFormat,
/// Output call receipts
#[clap(long, short = 'r', alias = "receipts")]
/// Show transaction receipts in the output
#[clap(long, short = 'r', alias = "receipts", help_heading = "OUTPUT")]
pub show_receipts: bool,
}
impl Command {
/// Validate the command and determine the CLI operation
pub fn validate_and_get_operation(&self) -> Result<Operation, String> {
// Case 1: List functions
if self.list_functions {
let Some(abi) = &self.abi else {
return Err("ABI is required when using --list-functions".to_string());
};
return Ok(Operation::ListFunctions {
contract_id: (*self.address).into(),
abi: abi.to_owned(),
});
}
// Case 2: Direct transfer with amount
if self.function.is_none() && self.call_parameters.amount > 0 {
if self.mode != ExecutionMode::Live {
return Err("Direct transfers are only supported in live mode".to_string());
}
return Ok(Operation::DirectTransfer {
recipient: (*self.address).into(),
amount: self.call_parameters.amount,
asset_id: self.call_parameters.asset_id,
});
}
// Case 3: Call function
if let Some(function) = &self.function {
let Some(abi) = &self.abi else {
return Err("ABI is required when calling a function".to_string());
};
let func_type = FuncType::from_str(function)?;
return Ok(Operation::CallFunction {
contract_id: (*self.address).into(),
abi: abi.to_owned(),
function: func_type,
function_args: self.function_args.to_owned(),
});
}
// No valid operation matched
Err("Either function selector, --list-functions flag, or non-zero --amount for direct transfers must be provided".to_string())
}
}
fn parse_abi_path(s: &str) -> Result<Either<PathBuf, Url>, String> {
if let Ok(url) = Url::parse(s) {
match url.scheme() {

View file

@ -0,0 +1,804 @@
use crate::{
cmd::{self, call::FuncType},
op::call::{
missing_contracts::get_missing_contracts,
parser::{param_type_val_to_token, token_to_string},
CallResponse, Either,
},
};
use anyhow::{anyhow, bail, Result};
use fuel_abi_types::abi::{program::ProgramABI, unified_program::UnifiedProgramABI};
use fuels::{
accounts::ViewOnlyAccount,
programs::calls::{
receipt_parser::ReceiptParser,
traits::{ContractDependencyConfigurator, TransactionTuner},
ContractCall,
},
};
use fuels_core::{
codec::{
encode_fn_selector, log_formatters_lookup, ABIDecoder, ABIEncoder, DecoderConfig,
EncoderConfig, LogDecoder,
},
types::{
bech32::Bech32ContractId,
param_types::ParamType,
transaction::Transaction,
transaction_builders::{BuildableTransaction, ScriptBuildStrategy, VariableOutputPolicy},
ContractId,
},
};
use std::{collections::HashMap, path::PathBuf};
use url::Url;
/// Calls a contract function with the given parameters
pub async fn call_function(
contract_id: ContractId,
abi: Either<PathBuf, Url>,
function: FuncType,
function_args: Vec<String>,
cmd: cmd::Call,
) -> Result<CallResponse> {
let cmd::Call {
node,
mode,
caller,
call_parameters,
gas,
show_receipts,
output,
external_contracts,
..
} = cmd;
// Load ABI (already provided in the operation)
let abi_str = super::load_abi(&abi).await?;
let parsed_abi: ProgramABI = serde_json::from_str(&abi_str)?;
let unified_program_abi = UnifiedProgramABI::from_counterpart(&parsed_abi)?;
let cmd::call::FuncType::Selector(selector) = function;
let (encoded_data, output_param) =
prepare_contract_call_data(&unified_program_abi, &selector, &function_args)?;
// Setup connection to node
let (wallet, tx_policies, base_asset_id) = super::setup_connection(&node, caller, &gas).await?;
let call_parameters = cmd::call::CallParametersOpts {
asset_id: call_parameters.asset_id.or(Some(base_asset_id)),
..call_parameters
};
// Create the contract call
let call = ContractCall {
contract_id: contract_id.into(),
encoded_selector: encode_fn_selector(&selector),
encoded_args: Ok(encoded_data),
call_parameters: call_parameters.clone().into(),
external_contracts: vec![], // set below
output_param: output_param.clone(),
is_payable: call_parameters.amount > 0,
custom_assets: Default::default(),
inputs: Vec::new(),
outputs: Vec::new(),
};
// Setup variable output policy and log decoder
let variable_output_policy = VariableOutputPolicy::Exactly(call_parameters.amount as usize);
let log_decoder = LogDecoder::new(log_formatters_lookup(vec![], contract_id));
// Get external contracts (either provided or auto-detected)
let external_contracts = match external_contracts {
Some(external_contracts) => external_contracts
.iter()
.map(|addr| Bech32ContractId::from(*addr))
.collect(),
None => {
// Automatically retrieve missing contract addresses from the call
let external_contracts = get_missing_contracts(
call.clone(),
wallet.provider(),
&tx_policies,
&variable_output_policy,
&log_decoder,
&wallet,
None,
)
.await?;
if !external_contracts.is_empty() {
forc_tracing::println_warning(
"Automatically provided external contract addresses with call (max 10):",
);
external_contracts.iter().for_each(|addr| {
forc_tracing::println_warning(&format!("- 0x{}", ContractId::from(addr)));
});
}
external_contracts
}
};
// Execute the call based on execution mode
let chain_id = wallet.provider().consensus_parameters().await?.chain_id();
let tb = call
.clone()
.with_external_contracts(external_contracts)
.transaction_builder(tx_policies, variable_output_policy, &wallet)
.await
.map_err(|e| anyhow!("Failed to initialize transaction builder: {e}"))?;
let (tx_status, tx_hash) = match mode {
cmd::call::ExecutionMode::DryRun => {
let tx = call
.build_tx(tb, &wallet)
.await
.map_err(|e| anyhow!("Failed to build transaction: {e}"))?;
let tx_hash = tx.id(chain_id);
let tx_status = wallet
.provider()
.dry_run(tx)
.await
.map_err(|e| anyhow!("Failed to dry run transaction: {e}"))?;
(tx_status, tx_hash)
}
cmd::call::ExecutionMode::Simulate => {
let tb = tb.with_build_strategy(ScriptBuildStrategy::StateReadOnly);
let tx = call
.build_tx(tb, &wallet)
.await
.map_err(|e| anyhow!("Failed to build transaction: {e}"))?;
let tx_hash = tx.id(chain_id);
let gas_price = gas.map(|g| g.price).unwrap_or(Some(0));
let tx_status = wallet
.provider()
.dry_run_opt(tx, false, gas_price)
.await
.map_err(|e| anyhow!("Failed to simulate transaction: {e}"))?;
(tx_status, tx_hash)
}
cmd::call::ExecutionMode::Live => {
forc_tracing::println_action_green(
"Sending transaction with wallet",
&format!("0x{}", wallet.address().hash()),
);
let tx = call
.build_tx(tb, &wallet)
.await
.map_err(|e| anyhow!("Failed to build transaction: {e}"))?;
let tx_hash = tx.id(chain_id);
let tx_status = wallet
.provider()
.send_transaction_and_await_commit(tx)
.await
.map_err(|e| anyhow!("Failed to send transaction: {e}"))?;
(tx_status, tx_hash)
}
};
// Process transaction results
let receipts = tx_status
.take_receipts_checked(Some(&log_decoder))
.map_err(|e| anyhow!("Failed to take receipts: {e}"))?;
// Parse the result based on output format
let mut receipt_parser = ReceiptParser::new(&receipts, DecoderConfig::default());
let result = match output {
cmd::call::OutputFormat::Default => {
let data = receipt_parser
.extract_contract_call_data(contract_id)
.ok_or(anyhow!("Failed to extract contract call data"))?;
ABIDecoder::default()
.decode_as_debug_str(&output_param, data.as_slice())
.map_err(|e| anyhow!("Failed to decode as debug string: {e}"))?
}
cmd::call::OutputFormat::Raw => {
let token = receipt_parser
.parse_call(&Bech32ContractId::from(contract_id), &output_param)
.map_err(|e| anyhow!("Failed to parse call data: {e}"))?;
token_to_string(&token)
.map_err(|e| anyhow!("Failed to convert token to string: {e}"))?
}
};
// Process and return the final output
let program_abi = sway_core::asm_generation::ProgramABI::Fuel(parsed_abi);
super::process_transaction_output(
&receipts,
&tx_hash.to_string(),
&program_abi,
result,
&mode,
&node,
show_receipts,
)
}
fn prepare_contract_call_data(
unified_program_abi: &UnifiedProgramABI,
selector: &str,
function_args: &[String],
) -> Result<(Vec<u8>, ParamType)> {
let type_lookup = unified_program_abi
.types
.iter()
.map(|decl| (decl.type_id, decl.clone()))
.collect::<HashMap<_, _>>();
// Find the function in the ABI
let abi_func = unified_program_abi
.functions
.iter()
.find(|f| f.name == selector)
.cloned()
.ok_or_else(|| anyhow!("Function '{selector}' not found in ABI"))?;
// Validate number of arguments
if abi_func.inputs.len() != function_args.len() {
bail!(
"Argument count mismatch for '{selector}': expected {}, got {}",
abi_func.inputs.len(),
function_args.len()
);
}
// Parse function arguments to tokens
let tokens = abi_func
.inputs
.iter()
.zip(function_args)
.map(|(type_application, arg)| {
let param_type =
ParamType::try_from_type_application(type_application, &type_lookup)
.map_err(|e| anyhow!("Failed to convert input type application: {e}"))?;
param_type_val_to_token(&param_type, arg)
})
.collect::<Result<Vec<_>>>()?;
// Get output parameter type
let output_param = ParamType::try_from_type_application(&abi_func.output, &type_lookup)
.map_err(|e| anyhow!("Failed to convert output type: {e}"))?;
// Encode function arguments
let abi_encoder = ABIEncoder::new(EncoderConfig::default());
let encoded_data = abi_encoder
.encode(&tokens)
.map_err(|e| anyhow!("Failed to encode function arguments: {e}"))?;
Ok((encoded_data, output_param))
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::{
cmd,
op::call::{call, get_wallet, PrivateKeySigner},
};
use fuels::{crypto::SecretKey, prelude::*};
fn get_contract_call_cmd(
id: ContractId,
node_url: &str,
secret_key: SecretKey,
selector: &str,
args: Vec<&str>,
) -> cmd::Call {
cmd::Call {
address: (*id).into(),
abi: Some(Either::Left(std::path::PathBuf::from(
"../../forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-abi.json",
))),
function: Some(selector.to_string()),
function_args: args.into_iter().map(String::from).collect(),
node: crate::NodeTarget { node_url: Some(node_url.to_string()), ..Default::default() },
caller: cmd::call::Caller { signing_key: Some(secret_key), wallet: false },
call_parameters: Default::default(),
mode: cmd::call::ExecutionMode::DryRun,
gas: None,
external_contracts: None,
output: cmd::call::OutputFormat::Raw,
show_receipts: false,
list_functions: false,
}
}
abigen!(Contract(
name = "TestContract",
abi = "forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-abi.json"
));
pub async fn get_contract_instance() -> (TestContract<Wallet>, ContractId, Provider, SecretKey)
{
let secret_key = SecretKey::random(&mut rand::thread_rng());
let signer = PrivateKeySigner::new(secret_key);
let coins = setup_single_asset_coins(signer.address(), AssetId::zeroed(), 1, 1_000_000);
let provider = setup_test_provider(coins, vec![], None, None)
.await
.unwrap();
let wallet = get_wallet(Some(secret_key), false, provider.clone())
.await
.unwrap();
let id = Contract::load_from(
"../../forc-plugins/forc-client/test/data/contract_with_types/contract_with_types.bin",
LoadConfiguration::default(),
)
.unwrap()
.deploy(&wallet, TxPolicies::default())
.await
.unwrap()
.contract_id;
let instance = TestContract::new(id.clone(), wallet.clone());
(instance, id.into(), provider, secret_key)
}
#[tokio::test]
async fn contract_call_with_abi() {
let (_, id, provider, secret_key) = get_contract_instance().await;
let node_url = provider.url();
// test_empty_no_return
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_empty_no_return", vec![]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "()");
// test_empty
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_empty", vec![]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "()");
// test_unit
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_unit", vec!["()"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "()");
// test_u8
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_u8", vec!["255"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "255");
// test_u16
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_u16", vec!["65535"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "65535");
// test_u32
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_u32", vec!["4294967295"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "4294967295");
// test_u64
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_u64",
vec!["18446744073709551615"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap().result,
"18446744073709551615"
);
// test_u128
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_u128",
vec!["340282366920938463463374607431768211455"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap().result,
"340282366920938463463374607431768211455"
);
// test_u256
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_u256",
vec!["115792089237316195423570985008687907853269984665640564039457584007913129639935"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap().result,
"115792089237316195423570985008687907853269984665640564039457584007913129639935"
);
// test b256
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_b256",
vec!["0000000000000000000000000000000000000000000000000000000000000042"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap().result,
"0x0000000000000000000000000000000000000000000000000000000000000042"
);
// test_b256 - fails if 0x prefix provided since it extracts input as an external contract; we don't want to do this so explicitly provide the external contract as empty
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_b256",
vec!["0x0000000000000000000000000000000000000000000000000000000000000042"],
);
let operation = cmd.validate_and_get_operation().unwrap();
cmd.external_contracts = Some(vec![]);
assert_eq!(
call(operation, cmd).await.unwrap().result,
"0x0000000000000000000000000000000000000000000000000000000000000042"
);
// test_bytes
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_bytes", vec!["0x42"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "0x42");
// test bytes without 0x prefix
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_bytes", vec!["42"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "0x42");
// test_str
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_str", vec!["fuel"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "fuel");
// test str array
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_str_array",
vec!["fuel rocks"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "fuel rocks");
// test str array - fails if length mismatch
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_str_array", vec!["fuel"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap_err().to_string(),
"string array length mismatch: expected 10, got 4"
);
// test str slice
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_str_slice",
vec!["fuel rocks 42"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "fuel rocks 42");
// test tuple
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_tuple", vec!["(42, true)"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "(42, true)");
// test array
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_array",
vec!["[42, 42, 42, 42, 42, 42, 42, 42, 42, 42]"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap().result,
"[42, 42, 42, 42, 42, 42, 42, 42, 42, 42]"
);
// test_array - fails if different types
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_array", vec!["[42, true]"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap_err().to_string(),
"failed to parse u64 value: true"
);
// test_array - succeeds if length not matched!?
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_array", vec!["[42, 42]"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert!(call(operation, cmd)
.await
.unwrap()
.result
.starts_with("[42, 42, 0,"));
// test_vector
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_vector", vec!["[42, 42]"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "[42, 42]");
// test_vector - fails if different types
let cmd =
get_contract_call_cmd(id, node_url, secret_key, "test_vector", vec!["[42, true]"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap_err().to_string(),
"failed to parse u64 value: true"
);
// test_struct - Identity { name: str[2], id: u64 }
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_struct", vec!["{fu, 42}"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "{fu, 42}");
// test_struct - fails if incorrect inner attribute length
let cmd =
get_contract_call_cmd(id, node_url, secret_key, "test_struct", vec!["{fuel, 42}"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap_err().to_string(),
"string array length mismatch: expected 2, got 4"
);
// test_struct - succeeds if missing inner final attribute; default value is used
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_struct", vec!["{fu}"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "{fu, 0}");
// test_struct - succeeds to use default values for all attributes if missing
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_struct", vec!["{}"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "{\0\0, 0}");
// test_enum
let cmd =
get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(Active:true)"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "(Active:true)");
// test_enum - succeeds if using index
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(1:56)"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "(Pending:56)");
// test_enum - fails if variant not found
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(A:true)"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap_err().to_string(),
"failed to find index of variant: A"
);
// test_enum - fails if variant value incorrect
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(Active:3)"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap_err().to_string(),
"failed to parse `Active` variant enum value: 3"
);
// test_enum - fails if variant value is missing
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(Active:)"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap_err().to_string(),
"enum must have exactly two parts `(variant:value)`: (Active:)"
);
// test_option - encoded like an enum
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_option", vec!["(0:())"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "(None:())");
// test_option - encoded like an enum; none value ignored
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_option", vec!["(0:42)"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "(None:())");
// test_option - encoded like an enum; some value
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_option", vec!["(1:42)"]);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "(Some:42)");
}
#[tokio::test]
async fn contract_call_with_abi_complex() {
let (_, id, provider, secret_key) = get_contract_instance().await;
let node_url = provider.url();
// test_complex_struct
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_struct_with_generic",
vec!["{42, fuel}"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "{42, fuel}");
// test_enum_with_generic
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_enum_with_generic",
vec!["(value:32)"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "(value:32)");
// test_enum_with_complex_generic
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_enum_with_complex_generic",
vec!["(value:{42, fuel})"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap().result,
"(value:{42, fuel})"
);
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_enum_with_complex_generic",
vec!["(container:{{42, fuel}, fuel})"],
);
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(
call(operation, cmd).await.unwrap().result,
"(container:{{42, fuel}, fuel})"
);
}
#[tokio::test]
async fn contract_value_forwarding() {
let (_, id, provider, secret_key) = get_contract_instance().await;
let node_url = provider.url();
let consensus_parameters = provider.consensus_parameters().await.unwrap();
let base_asset_id = consensus_parameters.base_asset_id();
let get_recipient_balance = |addr: Bech32Address, provider: Provider| async move {
provider
.get_asset_balance(&addr, *base_asset_id)
.await
.unwrap()
};
let get_contract_balance = |id: ContractId, provider: Provider| async move {
provider
.get_contract_asset_balance(&Bech32ContractId::from(id), *base_asset_id)
.await
.unwrap()
};
// contract call transfer funds to another contract
let (_, id_2, _, _) = get_contract_instance().await;
let (amount, asset_id, recipient) = (
"1",
&format!("{{0x{}}}", base_asset_id),
&format!("(ContractId:{{0x{}}})", id_2),
);
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"transfer",
vec![amount, asset_id, recipient],
);
let operation = cmd.validate_and_get_operation().unwrap();
cmd.call_parameters = cmd::call::CallParametersOpts {
amount: amount.parse::<u64>().unwrap(),
asset_id: Some(*base_asset_id),
gas_forwarded: None,
};
// validate balance is unchanged (dry-run)
assert_eq!(
call(operation.clone(), cmd.clone()).await.unwrap().result,
"()"
);
assert_eq!(get_contract_balance(id_2, provider.clone()).await, 0);
cmd.mode = cmd::call::ExecutionMode::Live;
assert_eq!(call(operation, cmd).await.unwrap().result, "()");
assert_eq!(get_contract_balance(id_2, provider.clone()).await, 1);
assert_eq!(get_contract_balance(id, provider.clone()).await, 1);
// contract call transfer funds to another address
let random_wallet = Wallet::random(&mut rand::thread_rng(), provider.clone());
let (amount, asset_id, recipient) = (
"2",
&format!("{{0x{}}}", base_asset_id),
&format!("(Address:{{0x{}}})", random_wallet.address().hash()),
);
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"transfer",
vec![amount, asset_id, recipient],
);
cmd.call_parameters = cmd::call::CallParametersOpts {
amount: amount.parse::<u64>().unwrap(),
asset_id: Some(*base_asset_id),
gas_forwarded: None,
};
cmd.mode = cmd::call::ExecutionMode::Live;
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "()");
assert_eq!(
get_recipient_balance(random_wallet.address().clone(), provider.clone()).await,
2
);
assert_eq!(get_contract_balance(id, provider.clone()).await, 1);
// contract call transfer funds to another address
// specify amount x, provide amount x - 1
// fails with panic reason 'NotEnoughBalance'
let random_wallet = Wallet::random(&mut rand::thread_rng(), provider.clone());
let (amount, asset_id, recipient) = (
"5",
&format!("{{0x{}}}", base_asset_id),
&format!("(Address:{{0x{}}})", random_wallet.address().hash()),
);
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"transfer",
vec![amount, asset_id, recipient],
);
cmd.call_parameters = cmd::call::CallParametersOpts {
amount: amount.parse::<u64>().unwrap() - 3,
asset_id: Some(*base_asset_id),
gas_forwarded: None,
};
cmd.mode = cmd::call::ExecutionMode::Live;
let operation = cmd.validate_and_get_operation().unwrap();
assert!(call(operation, cmd)
.await
.unwrap_err()
.to_string()
.contains("PanicInstruction { reason: NotEnoughBalance"));
assert_eq!(get_contract_balance(id, provider.clone()).await, 1);
// contract call transfer funds to another address
// specify amount x, provide amount x + 5; should succeed
let random_wallet = Wallet::random(&mut rand::thread_rng(), provider.clone());
let (amount, asset_id, recipient) = (
"3",
&format!("{{0x{}}}", base_asset_id),
&format!("(Address:{{0x{}}})", random_wallet.address().hash()),
);
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"transfer",
vec![amount, asset_id, recipient],
);
cmd.call_parameters = cmd::call::CallParametersOpts {
amount: amount.parse::<u64>().unwrap() + 5,
asset_id: Some(*base_asset_id),
gas_forwarded: None,
};
cmd.mode = cmd::call::ExecutionMode::Live;
let operation = cmd.validate_and_get_operation().unwrap();
assert_eq!(call(operation, cmd).await.unwrap().result, "()");
assert_eq!(
get_recipient_balance(random_wallet.address().clone(), provider.clone()).await,
3
);
assert_eq!(get_contract_balance(id, provider.clone()).await, 6); // extra amount (5) is forwarded to the contract
}
}

View file

@ -23,95 +23,95 @@ pub fn list_contract_functions<W: Write>(
if unified_program_abi.functions.is_empty() {
writeln!(writer, "No functions found in the contract ABI.")?;
} else {
let type_lookup = unified_program_abi
.types
.iter()
.map(|decl| (decl.type_id, decl.clone()))
.collect::<HashMap<_, _>>();
return Ok(());
}
for func in &unified_program_abi.functions {
let func_args = func
.inputs
.iter()
.map(|input| {
let Ok(param_type) = ParamType::try_from_type_application(input, &type_lookup)
else {
return Err(anyhow!("Failed to convert input type application"));
};
let func_args =
format!("{}: {}", input.name, param_to_function_arg(&param_type));
let func_args_input = {
let token =
param_type_val_to_token(&param_type, &get_default_value(&param_type))
.map_err(|err| {
let type_lookup = unified_program_abi
.types
.iter()
.map(|decl| (decl.type_id, decl.clone()))
.collect::<HashMap<_, _>>();
for func in &unified_program_abi.functions {
let func_args = func
.inputs
.iter()
.map(|input| {
let Ok(param_type) = ParamType::try_from_type_application(input, &type_lookup)
else {
return Err(anyhow!("Failed to convert input type application"));
};
let func_args = format!("{}: {}", input.name, param_to_function_arg(&param_type));
let func_args_input = {
let token =
param_type_val_to_token(&param_type, &get_default_value(&param_type))
.map_err(|err| {
anyhow!(
"Failed to generate example call for {}: {}",
func.name,
err
)
})?;
token_to_string(&token).map_err(|err| {
anyhow!(
"Failed to convert token to string for {}: {}",
func.name,
err
)
})?
};
Ok((func_args, func_args_input, param_type))
})
.collect::<Result<Vec<_>>>()?;
token_to_string(&token).map_err(|err| {
anyhow!(
"Failed to convert token to string for {}: {}",
func.name,
err
)
})?
};
Ok((func_args, func_args_input, param_type))
})
.collect::<Result<Vec<_>>>()?;
let func_args_types = func_args
.iter()
.map(|(func_args, _, _)| func_args.to_owned())
.collect::<Vec<String>>()
.join(", ");
let func_args_types = func_args
.iter()
.map(|(func_args, _, _)| func_args.to_owned())
.collect::<Vec<String>>()
.join(", ");
let func_args_inputs = func_args
.iter()
.map(|(_, func_args_input, param_type)| match param_type {
ParamType::Array(_, _)
| ParamType::Unit
| ParamType::Tuple(_)
| ParamType::Struct { .. }
| ParamType::Enum { .. }
| ParamType::RawSlice
| ParamType::Vector(_) => format!("\"{}\"", func_args_input),
_ => func_args_input.to_owned(),
})
.collect::<Vec<String>>()
.join(" ");
let func_args_inputs = func_args
.iter()
.map(|(_, func_args_input, param_type)| match param_type {
ParamType::Array(_, _)
| ParamType::Unit
| ParamType::Tuple(_)
| ParamType::Struct { .. }
| ParamType::Enum { .. }
| ParamType::RawSlice
| ParamType::Vector(_) => format!("\"{}\"", func_args_input),
_ => func_args_input.to_owned(),
})
.collect::<Vec<String>>()
.join(" ");
let return_type = ParamType::try_from_type_application(&func.output, &type_lookup)
.map(|param_type| param_to_function_arg(&param_type))
.map_err(|err| {
anyhow!(
"Failed to convert output type application for {}: {}",
func.name,
err
)
})?;
let return_type = ParamType::try_from_type_application(&func.output, &type_lookup)
.map(|param_type| param_to_function_arg(&param_type))
.map_err(|err| {
anyhow!(
"Failed to convert output type application for {}: {}",
func.name,
err
)
})?;
// Get the ABI path or URL as a string
let raw_abi_input = match abi {
Either::Left(path) => path.to_str().unwrap_or("").to_owned(),
Either::Right(url) => url.to_string(),
};
// Get the ABI path or URL as a string
let raw_abi_input = match abi {
Either::Left(path) => path.to_str().unwrap_or("").to_owned(),
Either::Right(url) => url.to_string(),
};
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",
raw_abi_input, contract_id, func.name, func_args_inputs,
)?;
}
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",
raw_abi_input, contract_id, func.name, func_args_inputs,
)?;
}
Ok(())

View file

@ -1,45 +1,30 @@
mod call_function;
mod list_functions;
mod missing_contracts;
mod parser;
mod transfer;
use crate::{
cmd,
constants::DEFAULT_PRIVATE_KEY,
op::call::{
list_functions::list_contract_functions,
missing_contracts::get_missing_contracts,
parser::{param_type_val_to_token, token_to_string},
call_function::call_function, list_functions::list_contract_functions, transfer::transfer,
},
util::tx::{prompt_forc_wallet_password, select_local_wallet_account},
};
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, Result};
use either::Either;
use fuel_abi_types::abi::{program::ProgramABI, unified_program::UnifiedProgramABI};
use fuel_tx::Receipt;
use fuels::{
accounts::{provider::Provider, signers::private_key::PrivateKeySigner, wallet::Wallet},
accounts::{
provider::Provider, signers::private_key::PrivateKeySigner, wallet::Wallet, ViewOnlyAccount,
},
crypto::SecretKey,
programs::calls::{
receipt_parser::ReceiptParser,
traits::{ContractDependencyConfigurator, TransactionTuner},
ContractCall,
},
types::param_types::ParamType,
};
use fuels_accounts::ViewOnlyAccount;
use fuels_core::{
codec::{
encode_fn_selector, log_formatters_lookup, ABIDecoder, ABIEncoder, DecoderConfig,
EncoderConfig, LogDecoder,
},
types::{
bech32::Bech32ContractId,
transaction::{Transaction, TxPolicies},
transaction_builders::{BuildableTransaction, ScriptBuildStrategy, VariableOutputPolicy},
ContractId,
},
};
use std::{collections::HashMap, str::FromStr};
use fuels_core::types::{transaction::TxPolicies, AssetId};
use std::str::FromStr;
use sway_core;
#[derive(Debug, Default)]
pub struct CallResponse {
@ -49,272 +34,99 @@ pub struct CallResponse {
}
/// A command for calling a contract function.
pub async fn call(cmd: cmd::Call) -> anyhow::Result<CallResponse> {
let cmd::Call {
contract_id,
abi,
function,
function_args,
node,
caller,
call_parameters,
mode,
list_functions,
gas,
external_contracts,
output,
show_receipts,
} = cmd;
let abi_str = match &abi {
// TODO: add support for fetching verified ABI from registry (forc.pub)
// - This should be the default behaviour if no ABI is provided
// ↳ gh issue: https://github.com/FuelLabs/sway/issues/6893
Either::Left(path) => std::fs::read_to_string(path)?,
Either::Right(url) => {
let response = reqwest::get(url.clone()).await?.bytes().await?;
String::from_utf8(response.to_vec())?
pub async fn call(operation: cmd::call::Operation, cmd: cmd::Call) -> anyhow::Result<CallResponse> {
match operation {
cmd::call::Operation::ListFunctions { contract_id, abi } => {
let abi_str = load_abi(&abi).await?;
let parsed_abi: ProgramABI = serde_json::from_str(&abi_str)?;
let unified_program_abi = UnifiedProgramABI::from_counterpart(&parsed_abi)?;
list_contract_functions(
&contract_id,
&abi,
&unified_program_abi,
&mut std::io::stdout(),
)?;
Ok(CallResponse::default())
}
};
let parsed_abi: ProgramABI = serde_json::from_str(&abi_str)?;
let unified_program_abi = UnifiedProgramABI::from_counterpart(&parsed_abi)?;
cmd::call::Operation::DirectTransfer {
recipient,
amount,
asset_id,
} => {
let cmd::Call {
node,
caller,
gas,
show_receipts,
..
} = cmd;
// If list_functions flag is set, print all available functions and return early
if list_functions {
list_contract_functions(
&contract_id,
&abi,
&unified_program_abi,
&mut std::io::stdout(),
)?;
return Ok(CallResponse::default());
// Already validated that mode is ExecutionMode::Live
let (wallet, tx_policies, base_asset_id) =
setup_connection(&node, caller, &gas).await?;
let asset_id = asset_id.unwrap_or(base_asset_id);
let response = transfer(
&wallet,
recipient,
amount,
asset_id,
tx_policies,
show_receipts,
&node,
)
.await?;
Ok(response)
}
cmd::call::Operation::CallFunction {
contract_id,
abi,
function,
function_args,
} => {
// Call the function with required parameters
let result = call_function(contract_id, abi, function, function_args, cmd).await?;
Ok(result)
}
}
}
let selector = match function {
Some(cmd::call::FuncType::Selector(selector)) => selector,
None => bail!("Function selector is required when --list-functions is not specified"),
};
let type_lookup = unified_program_abi
.types
.into_iter()
.map(|decl| (decl.type_id, decl))
.collect::<HashMap<_, _>>();
// get the function selector from the abi
let abi_func = unified_program_abi
.functions
.iter()
.find(|abi_func| abi_func.name == selector)
.ok_or_else(|| anyhow!("Function '{}' not found in ABI", selector))?;
if abi_func.inputs.len() != function_args.len() {
bail!("Number of arguments does not match number of parameters in function signature; expected {}, got {}", abi_func.inputs.len(), function_args.len());
}
let tokens = abi_func
.inputs
.iter()
.zip(&function_args)
.map(|(type_application, arg)| {
let param_type = ParamType::try_from_type_application(type_application, &type_lookup)
.expect("Failed to convert input type application");
param_type_val_to_token(&param_type, arg)
})
.collect::<Result<Vec<_>>>()?;
let output_param = ParamType::try_from_type_application(&abi_func.output, &type_lookup)
.expect("Failed to convert output type");
let abi_encoder = ABIEncoder::new(EncoderConfig::default());
let encoded_data = abi_encoder.encode(&tokens)?;
// Create and execute call
let call = ContractCall {
contract_id: contract_id.into(),
encoded_selector: encode_fn_selector(&selector),
encoded_args: Ok(encoded_data),
call_parameters: call_parameters.clone().into(),
external_contracts: vec![], // set below
output_param: output_param.clone(),
is_payable: call_parameters.amount > 0,
custom_assets: Default::default(),
inputs: Vec::new(),
outputs: Vec::new(),
};
/// Sets up the connection to the node and initializes common parameters
async fn setup_connection(
node: &crate::NodeTarget,
caller: cmd::call::Caller,
gas: &Option<forc_tx::Gas>,
) -> anyhow::Result<(Wallet, TxPolicies, AssetId)> {
let node_url = node.get_node_url(&None)?;
let provider = Provider::connect(node_url).await?;
let wallet = get_wallet(caller.signing_key, caller.wallet, provider).await?;
let provider = wallet.provider();
let tx_policies = gas.as_ref().map(Into::into).unwrap_or_default();
let consensus_parameters = provider.consensus_parameters().await?;
let base_asset_id = consensus_parameters.base_asset_id();
let tx_policies = gas
.as_ref()
.map(Into::into)
.unwrap_or(TxPolicies::default());
let variable_output_policy = VariableOutputPolicy::Exactly(call_parameters.amount as usize);
let log_decoder = LogDecoder::new(log_formatters_lookup(vec![], contract_id));
let external_contracts = match external_contracts {
Some(external_contracts) => external_contracts
.iter()
.map(|addr| Bech32ContractId::from(*addr))
.collect(),
None => {
// Automatically retrieve missing contract addresses from the call - by simulating the call
// and checking for missing contracts in the receipts
// This makes the CLI more ergonomic
let external_contracts = get_missing_contracts(
call.clone(),
provider,
&tx_policies,
&variable_output_policy,
&log_decoder,
&wallet,
None,
)
.await?;
if !external_contracts.is_empty() {
forc_tracing::println_warning(
"Automatically provided external contract addresses with call (max 10):",
);
external_contracts.iter().for_each(|addr| {
forc_tracing::println_warning(&format!("- 0x{}", ContractId::from(addr)));
});
}
external_contracts
}
};
Ok((wallet, tx_policies, *base_asset_id))
}
let chain_id = consensus_parameters.chain_id();
let tb = call
.clone()
.with_external_contracts(external_contracts)
.transaction_builder(tx_policies, variable_output_policy, &wallet)
.await
.expect("Failed to initialize transaction builder");
let (tx_status, tx_hash) = match mode {
cmd::call::ExecutionMode::DryRun => {
let tx = call
.build_tx(tb, &wallet)
/// Helper function to load ABI from file or URL
async fn load_abi(abi: &Either<std::path::PathBuf, url::Url>) -> anyhow::Result<String> {
match abi {
Either::Left(path) => std::fs::read_to_string(path)
.map_err(|e| anyhow!("Failed to read ABI file at {:?}: {}", path, e)),
Either::Right(url) => {
let response = reqwest::get(url.clone())
.await
.expect("Failed to build transaction");
let tx_hash = tx.id(chain_id);
let tx_status = provider
.dry_run(tx)
.map_err(|e| anyhow!("Failed to fetch ABI from URL {}: {}", url, e))?;
let bytes = response
.bytes()
.await
.expect("Failed to dry run transaction");
(tx_status, tx_hash)
}
cmd::call::ExecutionMode::Simulate => {
forc_tracing::println_warning(&format!(
"Simulating transaction with wallet... {}",
wallet.address().hash()
));
let tb = tb.with_build_strategy(ScriptBuildStrategy::StateReadOnly);
let tx = call
.build_tx(tb, &wallet)
.await
.expect("Failed to build transaction");
let tx_hash = tx.id(chain_id);
let gas_price = gas.map(|g| g.price).unwrap_or(Some(0));
let tx_status = provider
.dry_run_opt(tx, false, gas_price)
.await
.expect("Failed to simulate transaction");
(tx_status, tx_hash)
}
cmd::call::ExecutionMode::Live => {
forc_tracing::println_action_green(
"Sending transaction with wallet",
&format!("0x{}", wallet.address().hash()),
);
let tx = call
.build_tx(tb, &wallet)
.await
.expect("Failed to build transaction");
let tx_hash = tx.id(chain_id);
let tx_status = provider
.send_transaction_and_await_commit(tx)
.await
.expect("Failed to send transaction");
(tx_status, tx_hash)
}
};
let receipts = tx_status
.take_receipts_checked(Some(&log_decoder))
.expect("Failed to take receipts");
let mut receipt_parser = ReceiptParser::new(&receipts, DecoderConfig::default());
let result = match output {
cmd::call::OutputFormat::Default => {
let data = receipt_parser
.extract_contract_call_data(contract_id)
.expect("Failed to extract contract call data");
ABIDecoder::default()
.decode_as_debug_str(&output_param, data.as_slice())
.expect("Failed to decode as debug string")
}
cmd::call::OutputFormat::Raw => {
let token = receipt_parser
.parse_call(&Bech32ContractId::from(contract_id), &output_param)
.expect("Failed to extract contract call data");
token_to_string(&token).expect("Failed to convert token to string")
}
};
// print receipts
if show_receipts {
let formatted_receipts = forc_util::tx_utils::format_log_receipts(&receipts, true)?;
forc_tracing::println_label_green("receipts:", &formatted_receipts);
}
// decode logs
let program_abi = sway_core::asm_generation::ProgramABI::Fuel(parsed_abi);
let logs = receipts
.iter()
.filter_map(|receipt| {
if let Receipt::LogData {
rb,
data: Some(data),
..
} = receipt
{
return forc_util::tx_utils::decode_log_data(&rb.to_string(), data, &program_abi)
.ok()
.map(|decoded| decoded.value);
}
None
})
.collect::<Vec<_>>();
// print logs
if !logs.is_empty() {
forc_tracing::println_green_bold("logs:");
for log in &logs {
println!(" {:#}", log);
.map_err(|e| anyhow!("Failed to read response body from URL {}: {}", url, e))?;
String::from_utf8(bytes.to_vec())
.map_err(|e| anyhow!("Failed to parse response as UTF-8 from URL {}: {}", url, e))
}
}
// print tx hash and result
forc_tracing::println_label_green("tx hash:", &tx_hash.to_string());
forc_tracing::println_label_green("result:", &result);
// display transaction url if live mode
if cmd::call::ExecutionMode::Live == mode {
if let Some(explorer_url) = node.get_explorer_url() {
forc_tracing::println_label_green(
"\nView transaction:",
&format!("{}/tx/0x{}\n", explorer_url, tx_hash),
);
}
}
Ok(CallResponse {
tx_hash: tx_hash.to_string(),
result,
logs,
})
}
/// Get the wallet to use for the call - based on optionally provided signing key and wallet flag.
@ -359,12 +171,72 @@ async fn get_wallet(
}
}
/// Processes transaction receipts, logs, and displays transaction information
pub(crate) fn process_transaction_output(
receipts: &[Receipt],
tx_hash: &str,
program_abi: &sway_core::asm_generation::ProgramABI,
result: String,
mode: &cmd::call::ExecutionMode,
node: &crate::NodeTarget,
show_receipts: bool,
) -> Result<CallResponse> {
// print receipts
if show_receipts {
let formatted_receipts = forc_util::tx_utils::format_log_receipts(receipts, true)?;
forc_tracing::println_label_green("receipts:", &formatted_receipts);
}
// decode logs
let logs = receipts
.iter()
.filter_map(|receipt| match receipt {
Receipt::LogData {
rb,
data: Some(data),
..
} => forc_util::tx_utils::decode_log_data(&rb.to_string(), data, program_abi)
.ok()
.map(|decoded| decoded.value),
_ => None,
})
.collect::<Vec<_>>();
// print logs
if !logs.is_empty() {
forc_tracing::println_green_bold("logs:");
for log in logs.iter() {
println!(" {:#}", log);
}
}
// print tx hash and result
forc_tracing::println_label_green("tx hash:", tx_hash);
if !result.is_empty() {
forc_tracing::println_label_green("result:", &result);
}
// display transaction url if live mode
if *mode == cmd::call::ExecutionMode::Live {
if let Some(explorer_url) = node.get_explorer_url() {
forc_tracing::println_label_green(
"\nView transaction:",
&format!("{}/tx/0x{}\n", explorer_url, tx_hash),
);
}
}
Ok(CallResponse {
tx_hash: tx_hash.to_string(),
result,
logs,
})
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use fuels::accounts::wallet::Wallet;
use fuels::prelude::*;
use rand::thread_rng;
abigen!(Contract(
name = "TestContract",
@ -397,458 +269,4 @@ pub(crate) mod tests {
(instance, id.into(), provider, secret_key)
}
fn get_contract_call_cmd(
id: ContractId,
node_url: &str,
secret_key: SecretKey,
selector: &str,
args: Vec<&str>,
) -> cmd::Call {
cmd::Call {
contract_id: id,
abi: Either::Left(std::path::PathBuf::from(
"../../forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-abi.json",
)),
function: Some(cmd::call::FuncType::Selector(selector.into())),
function_args: args.into_iter().map(String::from).collect(),
node: crate::NodeTarget { node_url: Some(node_url.to_string()), ..Default::default() },
caller: cmd::call::Caller { signing_key: Some(secret_key), wallet: false },
call_parameters: Default::default(),
mode: cmd::call::ExecutionMode::DryRun,
gas: None,
external_contracts: None,
output: cmd::call::OutputFormat::Raw,
show_receipts: false,
list_functions: false,
}
}
#[tokio::test]
async fn contract_call_with_abi() {
let (_, id, provider, secret_key) = get_contract_instance().await;
let node_url = provider.url();
// test_empty_no_return
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_empty_no_return", vec![]);
assert_eq!(call(cmd).await.unwrap().result, "()");
// test_empty
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_empty", vec![]);
assert_eq!(call(cmd).await.unwrap().result, "()");
// test_unit
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_unit", vec!["()"]);
assert_eq!(call(cmd).await.unwrap().result, "()");
// test_u8
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_u8", vec!["255"]);
assert_eq!(call(cmd).await.unwrap().result, "255");
// test_u16
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_u16", vec!["65535"]);
assert_eq!(call(cmd).await.unwrap().result, "65535");
// test_u32
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_u32", vec!["4294967295"]);
assert_eq!(call(cmd).await.unwrap().result, "4294967295");
// test_u64
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_u64",
vec!["18446744073709551615"],
);
assert_eq!(call(cmd).await.unwrap().result, "18446744073709551615");
// test_u128
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_u128",
vec!["340282366920938463463374607431768211455"],
);
assert_eq!(
call(cmd).await.unwrap().result,
"340282366920938463463374607431768211455"
);
// test_u256
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_u256",
vec!["115792089237316195423570985008687907853269984665640564039457584007913129639935"],
);
assert_eq!(
call(cmd).await.unwrap().result,
"115792089237316195423570985008687907853269984665640564039457584007913129639935"
);
// test b256
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_b256",
vec!["0000000000000000000000000000000000000000000000000000000000000042"],
);
assert_eq!(
call(cmd).await.unwrap().result,
"0x0000000000000000000000000000000000000000000000000000000000000042"
);
// test_b256 - fails if 0x prefix provided since it extracts input as an external contract; we don't want to do this so explicitly provide the external contract as empty
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_b256",
vec!["0x0000000000000000000000000000000000000000000000000000000000000042"],
);
cmd.external_contracts = Some(vec![]);
assert_eq!(
call(cmd).await.unwrap().result,
"0x0000000000000000000000000000000000000000000000000000000000000042"
);
// test_bytes
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_bytes", vec!["0x42"]);
assert_eq!(call(cmd).await.unwrap().result, "0x42");
// test bytes without 0x prefix
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_bytes", vec!["42"]);
assert_eq!(call(cmd).await.unwrap().result, "0x42");
// test_str
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_str", vec!["fuel"]);
assert_eq!(call(cmd).await.unwrap().result, "fuel");
// test str array
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_str_array",
vec!["fuel rocks"],
);
assert_eq!(call(cmd).await.unwrap().result, "fuel rocks");
// test str array - fails if length mismatch
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_str_array", vec!["fuel"]);
assert_eq!(
call(cmd).await.unwrap_err().to_string(),
"string array length mismatch: expected 10, got 4"
);
// test str slice
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_str_slice",
vec!["fuel rocks 42"],
);
assert_eq!(call(cmd).await.unwrap().result, "fuel rocks 42");
// test tuple
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_tuple", vec!["(42, true)"]);
assert_eq!(call(cmd).await.unwrap().result, "(42, true)");
// test array
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_array",
vec!["[42, 42, 42, 42, 42, 42, 42, 42, 42, 42]"],
);
assert_eq!(
call(cmd).await.unwrap().result,
"[42, 42, 42, 42, 42, 42, 42, 42, 42, 42]"
);
// test_array - fails if different types
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_array", vec!["[42, true]"]);
assert_eq!(
call(cmd).await.unwrap_err().to_string(),
"failed to parse u64 value: true"
);
// test_array - succeeds if length not matched!?
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_array", vec!["[42, 42]"]);
assert!(call(cmd).await.unwrap().result.starts_with("[42, 42, 0,"));
// test_vector
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_vector", vec!["[42, 42]"]);
assert_eq!(call(cmd).await.unwrap().result, "[42, 42]");
// test_vector - fails if different types
let cmd =
get_contract_call_cmd(id, node_url, secret_key, "test_vector", vec!["[42, true]"]);
assert_eq!(
call(cmd).await.unwrap_err().to_string(),
"failed to parse u64 value: true"
);
// test_struct - Identity { name: str[2], id: u64 }
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_struct", vec!["{fu, 42}"]);
assert_eq!(call(cmd).await.unwrap().result, "{fu, 42}");
// test_struct - fails if incorrect inner attribute length
let cmd =
get_contract_call_cmd(id, node_url, secret_key, "test_struct", vec!["{fuel, 42}"]);
assert_eq!(
call(cmd).await.unwrap_err().to_string(),
"string array length mismatch: expected 2, got 4"
);
// test_struct - succeeds if missing inner final attribute; default value is used
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_struct", vec!["{fu}"]);
assert_eq!(call(cmd).await.unwrap().result, "{fu, 0}");
// test_struct - succeeds to use default values for all attributes if missing
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_struct", vec!["{}"]);
assert_eq!(call(cmd).await.unwrap().result, "{\0\0, 0}");
// test_enum
let cmd =
get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(Active:true)"]);
assert_eq!(call(cmd).await.unwrap().result, "(Active:true)");
// test_enum - succeeds if using index
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(1:56)"]);
assert_eq!(call(cmd).await.unwrap().result, "(Pending:56)");
// test_enum - fails if variant not found
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(A:true)"]);
assert_eq!(
call(cmd).await.unwrap_err().to_string(),
"failed to find index of variant: A"
);
// test_enum - fails if variant value incorrect
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(Active:3)"]);
assert_eq!(
call(cmd).await.unwrap_err().to_string(),
"failed to parse `Active` variant enum value: 3"
);
// test_enum - fails if variant value is missing
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_enum", vec!["(Active:)"]);
assert_eq!(
call(cmd).await.unwrap_err().to_string(),
"enum must have exactly two parts `(variant:value)`: (Active:)"
);
// test_option - encoded like an enum
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_option", vec!["(0:())"]);
assert_eq!(call(cmd).await.unwrap().result, "(None:())");
// test_option - encoded like an enum; none value ignored
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_option", vec!["(0:42)"]);
assert_eq!(call(cmd).await.unwrap().result, "(None:())");
// test_option - encoded like an enum; some value
let cmd = get_contract_call_cmd(id, node_url, secret_key, "test_option", vec!["(1:42)"]);
assert_eq!(call(cmd).await.unwrap().result, "(Some:42)");
}
#[tokio::test]
async fn contract_call_with_abi_complex() {
let (_, id, provider, secret_key) = get_contract_instance().await;
let node_url = provider.url();
// test_complex_struct
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_struct_with_generic",
vec!["{42, fuel}"],
);
assert_eq!(call(cmd).await.unwrap().result, "{42, fuel}");
// test_enum_with_generic
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_enum_with_generic",
vec!["(value:32)"],
);
assert_eq!(call(cmd).await.unwrap().result, "(value:32)");
// test_enum_with_complex_generic
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_enum_with_complex_generic",
vec!["(value:{42, fuel})"],
);
assert_eq!(call(cmd).await.unwrap().result, "(value:{42, fuel})");
let cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"test_enum_with_complex_generic",
vec!["(container:{{42, fuel}, fuel})"],
);
assert_eq!(
call(cmd).await.unwrap().result,
"(container:{{42, fuel}, fuel})"
);
}
#[tokio::test]
async fn contract_value_forwarding() {
let (_, id, provider, secret_key) = get_contract_instance().await;
let wallet = get_wallet(Some(secret_key), false, provider).await.unwrap();
let provider = wallet.provider();
let node_url = provider.url();
let consensus_parameters = provider.consensus_parameters().await.unwrap();
let base_asset_id = consensus_parameters.base_asset_id();
let get_recipient_balance = |addr: Bech32Address| async move {
provider
.get_asset_balance(&addr, *base_asset_id)
.await
.unwrap()
};
let get_contract_balance = |id: ContractId| async move {
provider
.get_contract_asset_balance(&Bech32ContractId::from(id), *base_asset_id)
.await
.unwrap()
};
// contract call transfer funds to another address
let random_wallet = Wallet::random(&mut thread_rng(), provider.clone());
let (amount, asset_id, recipient) = (
"2",
&format!("{{0x{}}}", base_asset_id),
&format!("(Address:{{0x{}}})", random_wallet.address().hash()),
);
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"transfer",
vec![amount, asset_id, recipient],
);
cmd.call_parameters = cmd::call::CallParametersOpts {
amount: amount.parse::<u64>().unwrap(),
asset_id: Some(*base_asset_id),
gas_forwarded: None,
};
// validate balance is unchanged (dry-run)
assert_eq!(call(cmd.clone()).await.unwrap().result, "()");
assert_eq!(
get_recipient_balance(random_wallet.address().clone()).await,
0
);
// live call; balance should be updated
cmd.mode = cmd::call::ExecutionMode::Live;
assert_eq!(call(cmd).await.unwrap().result, "()");
assert_eq!(
get_recipient_balance(random_wallet.address().clone()).await,
2
);
assert_eq!(get_contract_balance(id).await, 0);
// contract call transfer funds to another contract
let (_, id_2, provider, secret_key) = get_contract_instance().await;
let (amount, asset_id, recipient) = (
"1",
&format!("{{0x{}}}", base_asset_id),
&format!("(ContractId:{{0x{}}})", id_2),
);
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"transfer",
vec![amount, asset_id, recipient],
);
cmd.call_parameters = cmd::call::CallParametersOpts {
amount: amount.parse::<u64>().unwrap(),
asset_id: Some(*base_asset_id),
gas_forwarded: None,
};
// validate balance is unchanged (dry-run)
assert_eq!(call(cmd.clone()).await.unwrap().result, "()");
assert_eq!(get_contract_balance(id_2).await, 0);
// live call; balance should be updated
cmd.mode = cmd::call::ExecutionMode::Live;
assert_eq!(call(cmd).await.unwrap().result, "()");
assert_eq!(get_contract_balance(id).await, 1);
assert_eq!(get_contract_balance(id_2).await, 1);
// contract call transfer funds to another address
// specify amount x, provide amount x - 1
// fails with panic reason 'NotEnoughBalance'
let random_wallet = Wallet::random(&mut thread_rng(), provider.clone());
let (amount, asset_id, recipient) = (
"5",
&format!("{{0x{}}}", base_asset_id),
&format!("(Address:{{0x{}}})", random_wallet.address().hash()),
);
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"transfer",
vec![amount, asset_id, recipient],
);
cmd.call_parameters = cmd::call::CallParametersOpts {
amount: amount.parse::<u64>().unwrap() - 3,
asset_id: Some(*base_asset_id),
gas_forwarded: None,
};
cmd.mode = cmd::call::ExecutionMode::Live;
assert!(call(cmd)
.await
.unwrap_err()
.to_string()
.contains("PanicInstruction { reason: NotEnoughBalance"));
assert_eq!(get_contract_balance(id).await, 1);
// contract call transfer funds to another address
// specify amount x, provide amount x + 5; should succeed
let random_wallet = Wallet::random(&mut thread_rng(), provider.clone());
let (amount, asset_id, recipient) = (
"3",
&format!("{{0x{}}}", base_asset_id),
&format!("(Address:{{0x{}}})", random_wallet.address().hash()),
);
let mut cmd = get_contract_call_cmd(
id,
node_url,
secret_key,
"transfer",
vec![amount, asset_id, recipient],
);
cmd.call_parameters = cmd::call::CallParametersOpts {
amount: amount.parse::<u64>().unwrap() + 5,
asset_id: Some(*base_asset_id),
gas_forwarded: None,
};
cmd.mode = cmd::call::ExecutionMode::Live;
assert_eq!(call(cmd).await.unwrap().result, "()");
assert_eq!(
get_recipient_balance(random_wallet.address().clone()).await,
3
);
assert_eq!(get_contract_balance(id).await, 6); // extra amount (5) is forwarded to the contract
}
}

View file

@ -0,0 +1,189 @@
use anyhow::anyhow;
use fuel_abi_types::abi::program::ProgramABI;
use fuels::{
accounts::{wallet::Wallet, Account},
types::bech32::{Bech32Address, Bech32ContractId},
};
use fuels_core::types::{transaction::TxPolicies, Address, AssetId};
use sway_core;
pub async fn transfer(
wallet: &Wallet,
recipient: Address,
amount: u64,
asset_id: AssetId,
tx_policies: TxPolicies,
show_receipts: bool,
node: &crate::NodeTarget,
) -> anyhow::Result<super::CallResponse> {
let provider = wallet.provider();
// check is recipient is a user
let tx_response = if provider.is_user_account(*recipient).await? {
println!(
"\nTransferring {} 0x{} to recipient address 0x{}...\n",
amount, asset_id, recipient
);
wallet
.transfer(&recipient.into(), amount, asset_id, tx_policies)
.await
.map_err(|e| anyhow!("Failed to transfer funds to recipient: {}", e))?
} else {
println!(
"\nTransferring {} 0x{} to contract address 0x{}...\n",
amount, asset_id, recipient
);
let address: Bech32Address = recipient.into();
let contract_id = Bech32ContractId {
hrp: address.hrp,
hash: address.hash,
};
wallet
.force_transfer_to_contract(&contract_id, amount, asset_id, tx_policies)
.await
.map_err(|e| anyhow!("Failed to transfer funds to contract: {}", e))?
};
// We don't need to load the ABI for a simple transfer
let program_abi = sway_core::asm_generation::ProgramABI::Fuel(ProgramABI::default());
super::process_transaction_output(
&tx_response.tx_status.receipts,
&tx_response.tx_id.to_string(),
&program_abi,
"".to_string(),
&crate::cmd::call::ExecutionMode::Live,
node,
show_receipts,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{op::call::PrivateKeySigner, NodeTarget};
use fuels::prelude::*;
#[tokio::test]
async fn test_transfer_function_to_recipient() {
// Launch a local network and set up wallets
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new(
Some(2), /* Two wallets */
Some(1), /* Single coin (UTXO) */
Some(1_000_000_000), /* Amount per coin */
),
None,
None,
)
.await
.unwrap();
let wallet_sender = wallets.pop().unwrap();
let wallet_recipient = wallets.pop().unwrap();
let recipient_address = wallet_recipient.address().into();
let provider = wallet_sender.provider();
let consensus_parameters = provider.consensus_parameters().await.unwrap();
let base_asset_id = consensus_parameters.base_asset_id();
// Test helpers to get balances
let get_recipient_balance = |addr: Bech32Address| async move {
provider
.get_asset_balance(&addr, *base_asset_id)
.await
.unwrap()
};
// Get initial balance of recipient
let initial_balance = get_recipient_balance(wallet_recipient.address().clone()).await;
// Test parameters
let tx_policies = TxPolicies::default();
let amount = 100;
let node = NodeTarget {
node_url: Some(provider.url().to_string()),
..Default::default()
};
// should successfully transfer funds)
let result = transfer(
&wallet_sender,
recipient_address,
amount,
*base_asset_id,
tx_policies,
false, // show_receipts
&node,
)
.await
.unwrap();
// Verify response structure
assert!(
!result.tx_hash.is_empty(),
"Transaction hash should be returned"
);
assert_eq!(result.result, "", "Result should be empty string");
// Verify balance has increased by the transfer amount
assert_eq!(
get_recipient_balance(wallet_recipient.address().clone()).await,
initial_balance + amount,
"Balance should increase by transfer amount"
);
}
#[tokio::test]
async fn test_transfer_function_to_contract() {
let (_, id, provider, secret_key) = crate::op::call::tests::get_contract_instance().await;
let wallet = Wallet::new(PrivateKeySigner::new(secret_key), provider.clone());
let consensus_parameters = provider.clone().consensus_parameters().await.unwrap();
let base_asset_id = consensus_parameters.base_asset_id();
// Verify initial contract balance
let balance = provider
.get_contract_asset_balance(&Bech32ContractId::from(id), *base_asset_id)
.await
.unwrap();
assert_eq!(balance, 0, "Balance should be 0");
// Test parameters
let tx_policies = TxPolicies::default();
let amount = 100;
let node = NodeTarget {
node_url: Some(provider.url().to_string()),
..Default::default()
};
// should successfully transfer funds)
let result = transfer(
&wallet,
Address::new(id.into()),
amount,
*base_asset_id,
tx_policies,
false, // show_receipts
&node,
)
.await
.unwrap();
// Verify response structure
assert!(
!result.tx_hash.is_empty(),
"Transaction hash should be returned"
);
assert_eq!(result.result, "", "Result should be empty string");
// Verify balance has increased by the transfer amount
let balance = provider
.get_contract_asset_balance(&Bech32ContractId::from(id), *base_asset_id)
.await
.unwrap();
assert_eq!(
balance, amount,
"Balance should increase by transfer amount"
);
}
}