use crate::{
cmd,
util::{
pkg::built_pkgs_with_manifest,
tx::{TransactionBuilderExt, TX_SUBMIT_TIMEOUT_MS},
},
};
use anyhow::{bail, Context, Result};
use forc_pkg::{self as pkg, PackageManifestFile};
use fuel_core_client::client::types::TransactionStatus;
use fuel_core_client::client::FuelClient;
use fuel_tx::{Output, Salt, TransactionBuilder};
use fuel_vm::prelude::*;
use futures::FutureExt;
use pkg::BuiltPackage;
use std::time::Duration;
use std::{collections::BTreeMap, path::PathBuf};
use sway_core::language::parsed::TreeType;
use sway_core::BuildTarget;
use tracing::info;
#[derive(Debug)]
pub struct DeployedContract {
pub id: fuel_tx::ContractId,
}
type ContractSaltMap = BTreeMap<String, Salt>;
fn validate_and_parse_salts<'a>(
salt_args: Vec<String>,
manifests: impl Iterator<Item = &'a PackageManifestFile>,
) -> Result<ContractSaltMap> {
let mut contract_salt_map = BTreeMap::default();
for salt_arg in salt_args {
if let Some((given_contract_name, salt)) = salt_arg.split_once(':') {
let salt = salt
.parse::<Salt>()
.map_err(|e| anyhow::anyhow!(e))
.unwrap();
if let Some(old) = contract_salt_map.insert(given_contract_name.to_string(), salt) {
bail!("2 salts provided for contract '{given_contract_name}':\n {old}\n {salt}");
};
} else {
bail!("Invalid salt provided - salt must be in the form <CONTRACT_NAME>:<SALT> when deploying a workspace");
}
}
for manifest in manifests {
for (dep_name, contract_dep) in manifest.contract_deps() {
let dep_pkg_name = contract_dep.dependency.package().unwrap_or(dep_name);
if let Some(declared_salt) = contract_salt_map.get(dep_pkg_name) {
bail!(
"Redeclaration of salt using the option '--salt' while a salt exists for contract '{}' \
under the contract dependencies of the Forc.toml manifest for '{}'\n\
Existing salt: '0x{}',\nYou declared: '0x{}'\n",
dep_pkg_name,
manifest.project_name(),
contract_dep.salt,
declared_salt,
);
}
}
}
Ok(contract_salt_map)
}
pub async fn deploy(command: cmd::Deploy) -> Result<Vec<DeployedContract>> {
let mut contract_ids = Vec::new();
let curr_dir = if let Some(ref path) = command.pkg.path {
PathBuf::from(path)
} else {
std::env::current_dir()?
};
let build_opts = build_opts_from_cmd(&command);
let built_pkgs_with_manifest = built_pkgs_with_manifest(&curr_dir, build_opts)?;
let contract_salt_map = if let Some(salt_input) = &command.salt {
if built_pkgs_with_manifest.len() > 1 {
let map = validate_and_parse_salts(
salt_input.clone(),
built_pkgs_with_manifest
.iter()
.map(|bpwm| bpwm.package_manifest_file()),
)?;
Some(map)
} else {
if salt_input.len() > 1 {
bail!("More than 1 salt was specified when deploying a single contract");
}
let salt = salt_input[0]
.parse::<Salt>()
.map_err(|e| anyhow::anyhow!(e))
.unwrap();
let mut contract_salt_map = ContractSaltMap::default();
contract_salt_map.insert(
built_pkgs_with_manifest[0]
.package_manifest_file()
.project_name()
.to_string(),
salt,
);
Some(contract_salt_map)
}
} else {
None
};
for built in built_pkgs_with_manifest {
let member_manifest = built.package_manifest_file();
if member_manifest
.check_program_type(vec![TreeType::Contract])
.is_ok()
{
let salt = match (&contract_salt_map, command.random_salt) {
(Some(map), false) => {
if let Some(salt) = map.get(member_manifest.project_name()) {
*salt
} else {
Default::default()
}
}
(None, true) => rand::random(),
(None, false) => Default::default(),
(Some(_), true) => {
bail!("Both `--salt` and `--random-salt` were specified: must choose one")
}
};
let contract_id =
deploy_pkg(&command, member_manifest, built.built_package(), salt).await?;
contract_ids.push(contract_id);
}
}
Ok(contract_ids)
}
pub async fn deploy_pkg(
command: &cmd::Deploy,
manifest: &PackageManifestFile,
compiled: &BuiltPackage,
salt: Salt,
) -> Result<DeployedContract> {
let node_url = command
.node_url
.as_deref()
.or_else(|| manifest.network.as_ref().map(|nw| &nw.url[..]))
.unwrap_or(crate::default::NODE_URL);
let client = FuelClient::new(node_url)?;
let bytecode = &compiled.bytecode.bytes;
let mut storage_slots = compiled.storage_slots.clone();
storage_slots.sort();
let contract = Contract::from(bytecode.clone());
let root = contract.root();
let state_root = Contract::initial_state_root(storage_slots.iter());
let contract_id = contract.id(&salt, &root, &state_root);
info!("Contract id: 0x{}", hex::encode(contract_id));
let tx = TransactionBuilder::create(bytecode.as_slice().into(), salt, storage_slots.clone())
.gas_limit(command.gas.limit)
.gas_price(command.gas.price)
.maturity(u64::from(command.maturity.maturity))
.add_output(Output::contract_created(contract_id, state_root))
.finalize_signed(client.clone(), command.unsigned, command.signing_key)
.await?;
let tx = Transaction::from(tx);
let deployment_request = client.submit_and_await_commit(&tx).map(|res| match res {
Ok(logs) => match logs {
TransactionStatus::Submitted { .. } => {
bail!("contract {} deployment timed out", &contract_id);
}
TransactionStatus::Success { block_id, .. } => {
info!("contract {} deployed in block {}", &contract_id, &block_id);
Ok(contract_id)
}
e => {
bail!(
"contract {} failed to deploy due to an error: {:?}",
&contract_id,
e
)
}
},
Err(e) => bail!("{e}"),
});
let contract_id = tokio::time::timeout(
Duration::from_millis(TX_SUBMIT_TIMEOUT_MS),
deployment_request,
)
.await
.with_context(|| {
format!(
"Timed out waiting for contract {} to deploy. The transaction may have been dropped.",
&contract_id
)
})??;
Ok(DeployedContract { id: contract_id })
}
fn build_opts_from_cmd(cmd: &cmd::Deploy) -> 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(),
json_abi_with_callpaths: cmd.pkg.json_abi_with_callpaths,
},
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(),
finalized_asm: cmd.print.finalized_asm,
intermediate_asm: cmd.print.intermediate_asm,
ir: cmd.print.ir,
},
time_phases: cmd.print.time_phases,
minify: pkg::MinifyOpts {
json_abi: cmd.minify.json_abi,
json_storage_slots: cmd.minify.json_storage_slots,
},
build_profile: cmd.build_profile.build_profile.clone(),
release: cmd.build_profile.release,
error_on_warnings: cmd.build_profile.error_on_warnings,
binary_outfile: cmd.build_output.bin_file.clone(),
debug_outfile: cmd.build_output.debug_file.clone(),
build_target: BuildTarget::default(),
tests: false,
member_filter: pkg::MemberFilter::only_contracts(),
experimental_storage: cmd.build_profile.experimental_storage,
}
}
#[cfg(test)]
mod test {
use super::*;
fn setup_manifest_files() -> BTreeMap<String, PackageManifestFile> {
let mut contract_to_manifest = BTreeMap::default();
let manifests_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join("data");
for entry in manifests_dir.read_dir().unwrap() {
let manifest =
PackageManifestFile::from_file(entry.unwrap().path().join("Forc.toml")).unwrap();
contract_to_manifest.insert(manifest.project_name().to_string(), manifest);
}
contract_to_manifest
}
#[test]
fn test_parse_and_validate_salts_pass() {
let mut manifests = setup_manifest_files();
let mut expected = ContractSaltMap::new();
let mut salt_strs = vec![];
manifests.remove("contract_with_dep_with_salt_conflict");
manifests.remove("contract_with_dep");
for (index, manifest) in manifests.values().enumerate() {
let salt = "0x0000000000000000000000000000000000000000000000000000000000000000";
let salt_str = format!("{}:{salt}", manifest.project_name());
salt_strs.push(salt_str.to_string());
expected.insert(
manifest.project_name().to_string(),
salt.parse::<Salt>().unwrap(),
);
let got = validate_and_parse_salts(salt_strs.clone(), manifests.values()).unwrap();
assert_eq!(got.len(), index + 1);
assert_eq!(got, expected);
}
}
#[test]
fn test_parse_and_validate_salts_duplicate_salt_input() {
let manifests = setup_manifest_files();
let first_name = manifests.first_key_value().unwrap().0;
let salt: Salt = "0x0000000000000000000000000000000000000000000000000000000000000000"
.parse()
.unwrap();
let salt_str = format!("{first_name}:{salt}");
let err_message =
format!("2 salts provided for contract '{first_name}':\n {salt}\n {salt}");
assert_eq!(
validate_and_parse_salts(vec![salt_str.clone(), salt_str], manifests.values())
.unwrap_err()
.to_string(),
err_message,
);
}
#[test]
fn test_parse_single_salt_multiple_manifests_malformed_input() {
let manifests = setup_manifest_files();
let salt_str =
"contract_a=0x0000000000000000000000000000000000000000000000000000000000000000";
let err_message =
"Invalid salt provided - salt must be in the form <CONTRACT_NAME>:<SALT> when deploying a workspace";
assert_eq!(
validate_and_parse_salts(vec![salt_str.to_string()], manifests.values())
.unwrap_err()
.to_string(),
err_message,
);
}
#[test]
fn test_parse_multiple_salts_conflict() {
let manifests = setup_manifest_files();
let salt_str =
"contract_with_dep:0x0000000000000000000000000000000000000000000000000000000000000001";
let err_message =
"Redeclaration of salt using the option '--salt' while a salt exists for contract 'contract_with_dep' \
under the contract dependencies of the Forc.toml manifest for 'contract_with_dep_with_salt_conflict'\n\
Existing salt: '0x0000000000000000000000000000000000000000000000000000000000000000',\n\
You declared: '0x0000000000000000000000000000000000000000000000000000000000000001'\n";
assert_eq!(
validate_and_parse_salts(vec![salt_str.to_string()], manifests.values())
.unwrap_err()
.to_string(),
err_message,
);
}
}