mod encode;
use crate::{
cmd,
constants::TX_SUBMIT_TIMEOUT_MS,
util::{
pkg::built_pkgs,
tx::{prompt_forc_wallet_password, select_account, SignerSelectionMode},
},
};
use anyhow::{anyhow, bail, Context, Result};
use forc::cli::shared::IrCliOpt;
use forc_pkg::{self as pkg, fuel_core_not_running, DumpOpts, PackageManifestFile};
use forc_tracing::println_warning;
use forc_util::tx_utils::format_log_receipts;
use fuel_abi_types::abi::program::ProgramABI;
use fuel_core_client::client::FuelClient;
use fuel_tx::{ContractId, Transaction};
use fuels::{
programs::calls::{traits::TransactionTuner, ScriptCall},
types::{
transaction::TxPolicies,
transaction_builders::{BuildableTransaction, VariableOutputPolicy},
},
};
use fuels_accounts::{provider::Provider, Account, ViewOnlyAccount};
use pkg::BuiltPackage;
use std::time::Duration;
use std::{path::PathBuf, str::FromStr};
use sway_core::BuildTarget;
use sway_core::{language::parsed::TreeType, IrCli};
use tokio::time::timeout;
use tracing::info;
use self::encode::ScriptCallHandler;
pub struct RanScript {
pub receipts: Vec<fuel_tx::Receipt>,
}
pub async fn run(command: cmd::Run) -> Result<Vec<RanScript>> {
let mut command = command;
if command.unsigned {
println_warning("--unsigned flag is deprecated, please prefer using --default-signer. Assuming `--default-signer` is passed. This means your transaction will be signed by an account that is funded by fuel-core by default for testing purposes.");
command.default_signer = true;
}
let mut receipts = Vec::new();
let curr_dir = if let Some(path) = &command.pkg.path {
PathBuf::from(path)
} else {
std::env::current_dir().map_err(|e| anyhow!("{:?}", e))?
};
let build_opts = build_opts_from_cmd(&command);
let built_pkgs_with_manifest = built_pkgs(&curr_dir, &build_opts)?;
let wallet_mode = if command.default_signer || command.signing_key.is_some() {
SignerSelectionMode::Manual
} else {
let password = prompt_forc_wallet_password()?;
SignerSelectionMode::ForcWallet(password)
};
for built in built_pkgs_with_manifest {
if built
.descriptor
.manifest_file
.check_program_type(&[TreeType::Script])
.is_ok()
{
let pkg_receipts = run_pkg(
&command,
&built.descriptor.manifest_file,
&built,
&wallet_mode,
)
.await?;
receipts.push(pkg_receipts);
}
}
Ok(receipts)
}
fn tx_policies_from_cmd(cmd: &cmd::Run) -> TxPolicies {
let mut tx_policies = TxPolicies::default();
if let Some(max_fee) = cmd.gas.max_fee {
tx_policies = tx_policies.with_max_fee(max_fee);
}
if let Some(script_gas_limit) = cmd.gas.script_gas_limit {
tx_policies = tx_policies.with_script_gas_limit(script_gas_limit);
}
tx_policies
}
pub async fn run_pkg(
command: &cmd::Run,
manifest: &PackageManifestFile,
compiled: &BuiltPackage,
signer_mode: &SignerSelectionMode,
) -> Result<RanScript> {
let node_url = command.node.get_node_url(&manifest.network)?;
let provider = Provider::connect(node_url.clone()).await?;
let consensus_params = provider.consensus_parameters().await?;
let tx_count = 1;
let account = select_account(
signer_mode,
command.default_signer || command.unsigned,
command.signing_key,
&provider,
tx_count,
)
.await?;
let script_data = match (&command.data, &command.args) {
(None, Some(args)) => {
let minify_json_abi = true;
let package_json_abi = compiled
.json_abi_string(minify_json_abi)?
.ok_or_else(|| anyhow::anyhow!("Missing json abi string"))?;
let main_arg_handler = ScriptCallHandler::from_json_abi_str(&package_json_abi)?;
let args = args.iter().map(|arg| arg.as_str()).collect::<Vec<_>>();
main_arg_handler.encode_arguments(args.as_slice())?
}
(Some(_), Some(_)) => {
bail!("Both --args and --data provided, must choose one.")
}
_ => {
let input_data = command.data.as_deref().unwrap_or("");
let data = input_data.strip_prefix("0x").unwrap_or(input_data);
hex::decode(data).expect("Invalid hex")
}
};
let external_contracts = command
.contract
.as_ref()
.into_iter()
.flat_map(|contracts| contracts.iter())
.map(|contract| {
ContractId::from_str(contract)
.map_err(|e| anyhow!("Failed to parse contract id: {}", e))
})
.collect::<Result<Vec<ContractId>>>()?;
let script_binary = compiled.bytecode.bytes.clone();
let call = ScriptCall {
script_binary,
encoded_args: Ok(script_data),
inputs: vec![],
outputs: vec![],
external_contracts,
};
let tx_policies = tx_policies_from_cmd(command);
let mut tb = call.transaction_builder(
tx_policies,
VariableOutputPolicy::EstimateMinimum,
&consensus_params,
call.inputs.clone(),
&account,
)?;
account.add_witnesses(&mut tb)?;
account.adjust_for_fee(&mut tb, 0).await?;
let tx = tb.build(provider).await?;
if command.dry_run {
info!("{:?}", tx);
Ok(RanScript { receipts: vec![] })
} else {
let program_abi = match &compiled.program_abi {
sway_core::asm_generation::ProgramABI::Fuel(abi) => Some(abi),
_ => None,
};
let receipts = try_send_tx(
node_url.as_str(),
&tx.into(),
command.pretty_print,
command.simulate,
command.debug,
program_abi,
)
.await?;
Ok(RanScript { receipts })
}
}
async fn try_send_tx(
node_url: &str,
tx: &Transaction,
pretty_print: bool,
simulate: bool,
debug: bool,
abi: Option<&ProgramABI>,
) -> Result<Vec<fuel_tx::Receipt>> {
let client = FuelClient::new(node_url)?;
match client.health().await {
Ok(_) => timeout(
Duration::from_millis(TX_SUBMIT_TIMEOUT_MS),
send_tx(&client, tx, pretty_print, simulate, debug, abi),
)
.await
.with_context(|| format!("timeout waiting for {tx:?} to be included in a block"))?,
Err(_) => Err(fuel_core_not_running(node_url)),
}
}
async fn send_tx(
client: &FuelClient,
tx: &Transaction,
pretty_print: bool,
simulate: bool,
debug: bool,
abi: Option<&ProgramABI>,
) -> Result<Vec<fuel_tx::Receipt>> {
let outputs = {
if !simulate {
let status = client.submit_and_await_commit(tx).await?;
match status {
fuel_core_client::client::types::TransactionStatus::Success {
receipts, ..
} => receipts,
fuel_core_client::client::types::TransactionStatus::Failure {
receipts, ..
} => receipts,
_ => vec![],
}
} else {
let txs = vec![tx.clone()];
let receipts = client.dry_run(txs.as_slice()).await?;
let receipts = receipts
.first()
.map(|tx| &tx.result)
.map(|res| res.receipts());
match receipts {
Some(receipts) => receipts.to_vec(),
None => vec![],
}
}
};
if !outputs.is_empty() {
info!("{}", format_log_receipts(&outputs, pretty_print)?);
}
if debug {
start_debug_session(client, tx, abi).await?;
}
Ok(outputs)
}
async fn start_debug_session(
fuel_client: &FuelClient,
tx: &fuel_tx::Transaction,
program_abi: Option<&ProgramABI>,
) -> Result<()> {
let mut debugger = forc_debug::debugger::Debugger::from_client(fuel_client.clone())
.await
.map_err(|e| anyhow!("Failed to create debugger: {e}"))?;
let mut tx_file = tempfile::Builder::new()
.suffix(".json")
.tempfile()
.map_err(|e| anyhow!("Failed to create temp transaction file: {e}"))?;
serde_json::to_writer_pretty(&mut tx_file, tx)
.map_err(|e| anyhow!("Failed to write transaction to temp file: {e}"))?;
let mut abi_file = tempfile::Builder::new()
.suffix(".json")
.tempfile()
.map_err(|e| anyhow!("Failed to create temp ABI file: {e}"))?;
let tx_cmd = if let Some(abi) = program_abi {
serde_json::to_writer_pretty(&mut abi_file, &abi)
.map_err(|e| anyhow!("Failed to write ABI to temp file: {e}"))?;
format!(
"start_tx {} {}",
tx_file.path().to_string_lossy(),
abi_file.path().to_string_lossy()
)
} else {
format!("start_tx {}", tx_file.path().to_string_lossy())
};
let mut cli = forc_debug::cli::Cli::new()
.map_err(|e| anyhow!("Failed to create debug CLI interface: {e}"))?;
cli.run(&mut debugger, Some(tx_cmd))
.await
.map_err(|e| anyhow!("Interactive debugging session failed: {e}"))?;
Ok(())
}
fn build_opts_from_cmd(cmd: &cmd::Run) -> pkg::BuildOpts {
pkg::BuildOpts {
pkg: pkg::PkgOpts {
path: cmd.pkg.path.clone(),
offline: cmd.pkg.offline,
terse: cmd.pkg.terse,
locked: cmd.pkg.locked,
output_directory: cmd.pkg.output_directory.clone(),
ipfs_node: cmd.pkg.ipfs_node.clone().unwrap_or_default(),
},
print: pkg::PrintOpts {
ast: cmd.print.ast,
dca_graph: cmd.print.dca_graph.clone(),
dca_graph_url_format: cmd.print.dca_graph_url_format.clone(),
asm: cmd.print.asm(),
bytecode: cmd.print.bytecode,
bytecode_spans: false,
ir: cmd.print.ir(),
reverse_order: cmd.print.reverse_order,
},
verify_ir: cmd
.verify_ir
.as_ref()
.map_or(IrCli::default(), |opts| IrCliOpt::from(opts).0),
minify: pkg::MinifyOpts {
json_abi: cmd.minify.json_abi,
json_storage_slots: cmd.minify.json_storage_slots,
},
dump: DumpOpts::default(),
build_target: BuildTarget::default(),
build_profile: cmd.build_profile.build_profile.clone(),
release: cmd.build_profile.release,
error_on_warnings: cmd.build_profile.error_on_warnings,
time_phases: cmd.print.time_phases,
profile: cmd.print.profile,
metrics_outfile: cmd.print.metrics_outfile.clone(),
binary_outfile: cmd.build_output.bin_file.clone(),
debug_outfile: cmd.build_output.debug_file.clone(),
hex_outfile: cmd.build_output.hex_file.clone(),
tests: false,
member_filter: pkg::MemberFilter::only_scripts(),
experimental: cmd.experimental.experimental.clone(),
no_experimental: cmd.experimental.no_experimental.clone(),
no_output: false,
}
}