diff --git a/docs/book/spell-check-custom-words.txt b/docs/book/spell-check-custom-words.txt index 0d66d348464..1fc58dea387 100644 --- a/docs/book/spell-check-custom-words.txt +++ b/docs/book/spell-check-custom-words.txt @@ -215,4 +215,5 @@ booleans underflows Codec bool -str \ No newline at end of file +str +multisig \ No newline at end of file diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index 8f35fd1b121..986be423dac 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -114,6 +114,16 @@ forc-deploy --target beta-5 Since deploying and running projects on the testnet cost gas, you will need coins to pay for them. You can get some using the [testnet faucet](https://faucet-testnet.fuel.network/). +## Delayed transactions + +For delayed transactions, you can use the `--submit-only` flag. This flag allows you to submit the transaction without waiting for its finalization. + +One use case for this is multisig transactions, where a deployment transaction may stay in a pending state while waiting for all signatures. + +```sh +forc-deploy --submit-only +``` + ## Deployment Artifacts forc-deploy saves the details of each deployment in the `out/deployments` folder within the project's root directory. Below is an example of a deployment artifact: diff --git a/forc-plugins/forc-client/src/cmd/deploy.rs b/forc-plugins/forc-client/src/cmd/deploy.rs index cf1f26e2a2e..edb9e76d7f0 100644 --- a/forc-plugins/forc-client/src/cmd/deploy.rs +++ b/forc-plugins/forc-client/src/cmd/deploy.rs @@ -55,6 +55,9 @@ pub struct Command { /// Deprecated in favor of `--default-signer`. #[clap(long)] pub unsigned: bool, + /// Submit the deployment transaction(s) without waiting for execution to complete. + #[clap(long)] + pub submit_only: bool, /// Set the key to be used for signing. pub signing_key: Option, /// Sign the deployment transaction manually. diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index f41fd80532c..84ec1edf3cd 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -17,7 +17,7 @@ use forc_pkg::{self as pkg, PackageManifestFile}; use forc_tracing::{println_action_green, println_warning}; use forc_util::default_output_directory; use forc_wallet::utils::default_wallet_path; -use fuel_core_client::client::types::TransactionStatus; +use fuel_core_client::client::types::{ChainInfo, TransactionStatus}; use fuel_core_client::client::FuelClient; use fuel_crypto::fuel_types::ChainId; use fuel_tx::{Salt, Transaction}; @@ -52,7 +52,7 @@ pub struct DeploymentArtifact { chain_id: ChainId, contract_id: String, deployment_size: usize, - deployed_block_height: u32, + deployed_block_height: Option, } impl DeploymentArtifact { @@ -442,79 +442,84 @@ pub async fn deploy_pkg( let chain_info = client.chain_info().await?; let chain_id = chain_info.consensus_parameters.chain_id(); - 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_height, .. } => { - let pkg_name = manifest.project_name(); - let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet()); - let (contract_url, block_url) = match target.explorer_url() { - Some(explorer_url) => ( - format!("{explorer_url}/contract/0x"), - format!("{explorer_url}/block/"), - ), - None => ("".to_string(), "".to_string()), - }; - println_action_green( - "Finished", - &format!("deploying {pkg_name} {contract_url}{contract_id}"), - ); - let block_height_formatted = - match u32::from_str_radix(&block_height.to_string(), 16) { - Ok(decimal) => format!("{block_url}{decimal}"), - Err(_) => block_height.to_string(), - }; - - println_action_green("Deployed", &format!("in block {block_height_formatted}")); - + // If only submitting the transaction, don't wait for the deployment to complete + let contract_id: ContractId = if command.submit_only { + match client.submit(&tx).await { + Ok(transaction_id) => { // Create a deployment artifact. - let deployment_size = bytecode.len(); - let deployment_artifact = DeploymentArtifact { - transaction_id: format!("0x{}", tx.id(&chain_id)), - salt: format!("0x{}", salt), - network_endpoint: node_url.to_string(), - chain_id, - contract_id: format!("0x{}", contract_id), - deployment_size, - deployed_block_height: *block_height, - }; - - let output_dir = command - .pkg - .output_directory - .as_ref() - .map(PathBuf::from) - .unwrap_or_else(|| default_output_directory(manifest.dir())) - .join("deployments"); - deployment_artifact.to_file(&output_dir, pkg_name, contract_id)?; - - Ok(contract_id) + create_deployment_artifact( + DeploymentArtifact { + transaction_id: format!("0x{}", transaction_id), + salt: format!("0x{}", salt), + network_endpoint: node_url.to_string(), + chain_id, + contract_id: format!("0x{}", contract_id), + deployment_size: bytecode.len(), + deployed_block_height: None, + }, + command, + manifest, + chain_info, + )?; + + contract_id } - e => { + Err(e) => { bail!( "contract {} failed to deploy due to an error: {:?}", &contract_id, e ) } - }, - Err(e) => bail!("{e}"), - }); - - // submit contract deployment with a timeout - 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 + } + } else { + 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_height, .. } => { + // Create a deployment artifact. + create_deployment_artifact( + DeploymentArtifact { + transaction_id: format!("0x{}", tx.id(&chain_id)), + salt: format!("0x{}", salt), + network_endpoint: node_url.to_string(), + chain_id, + contract_id: format!("0x{}", contract_id), + deployment_size: bytecode.len(), + deployed_block_height: Some(*block_height), + }, + command, + manifest, + chain_info, + )?; + + Ok(contract_id) + } + e => { + bail!( + "contract {} failed to deploy due to an error: {:?}", + &contract_id, + e + ) + } + }, + Err(e) => bail!("{e}"), + }); + 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(contract_id) } @@ -559,6 +564,55 @@ fn build_opts_from_cmd(cmd: &cmd::Deploy) -> pkg::BuildOpts { } } +/// Creates a deployment artifact and writes it to a file. +/// +/// This function is used to generate a deployment artifact containing details +/// about the deployment, such as the transaction ID, salt, network endpoint, +/// chain ID, contract ID, deployment size, and deployed block height. It then +/// writes this artifact to a specified output directory. +fn create_deployment_artifact( + deployment_artifact: DeploymentArtifact, + cmd: &cmd::Deploy, + manifest: &PackageManifestFile, + chain_info: ChainInfo, +) -> Result<()> { + let contract_id = ContractId::from_str(&deployment_artifact.contract_id).unwrap(); + let pkg_name = manifest.project_name(); + + let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet()); + let (contract_url, block_url) = match target.explorer_url() { + Some(explorer_url) => ( + format!("{explorer_url}/contract/0x"), + format!("{explorer_url}/block/"), + ), + None => ("".to_string(), "".to_string()), + }; + println_action_green( + "Finished", + &format!("deploying {pkg_name} {contract_url}{contract_id}"), + ); + + let block_height = deployment_artifact.deployed_block_height; + if block_height.is_some() { + let block_height_formatted = + match u32::from_str_radix(&block_height.unwrap().to_string(), 16) { + Ok(decimal) => format!("{block_url}{decimal}"), + Err(_) => block_height.unwrap().to_string(), + }; + + println_action_green("Deployed", &format!("in block {block_height_formatted}")); + } + + let output_dir = cmd + .pkg + .output_directory + .as_ref() + .map(PathBuf::from) + .unwrap_or_else(|| default_output_directory(manifest.dir())) + .join("deployments"); + deployment_artifact.to_file(&output_dir, pkg_name, contract_id) +} + #[cfg(test)] mod test { use super::*; diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index e65e5e4f3cb..4e5ccd1ea48 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -150,6 +150,47 @@ async fn test_simple_deploy() { assert_eq!(contract_ids, expected) } +#[tokio::test] +async fn test_deploy_submit_only() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_data_path().join("standalone_contract"); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + + let target = NodeTarget { + node_url: Some(node_url), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + submit_only: true, + ..Default::default() + }; + let contract_ids = deploy(cmd).await.unwrap(); + node.kill().unwrap(); + let expected = vec![DeployedContract { + id: ContractId::from_str( + "ad0bba17e0838ef859abe2693d8a5e3bc4e7cfb901601e30f4dc34999fda6335", + ) + .unwrap(), + proxy: None, + }]; + + assert_eq!(contract_ids, expected) +} + #[tokio::test] async fn test_deploy_fresh_proxy() { let (mut node, port) = run_node();