Skip to content

Commit

Permalink
Allow forc deploy to submit transaction without waiting for commit co…
Browse files Browse the repository at this point in the history
…nfirmation (#6294)

## Description
The current `forc deploy` command is designed to submit and await for
transaction finalization as much this is a common use case, for delayed
transactions, were transaction are submitted but can take time a lot of
time to be included and finalized. For this use cases I have implement a
suggestion `--submit-only` this will follow the current flow, but only
submit the transaction.

### Example use case for delayed transactions
In Bako Safe, as it is a multisig, it is necessary to have signatures
from multiple accounts before sending the transaction to the network. To
work together with forc deploy, a GraphQL Proxy has been developed that
works with the Fuel provider to allow sending the transaction to our
protocol.

```zsh
forc deploy --node-url 'https://api.bako.global/v1/graphql' --default-signer --submit-only
```

## Changes
- [x] Add a new deploy params `--submit-only`
- [x] Create a branch on the code to verify if the param is provided
- [x] Abstract create artifacts to a single function to avoid code
repetition
- [x] Modified transaction submission to allow for immediate submission
when the `submit_only` command is provided
- [x] Implemented tests for the new command


## Checklist
- [ ] I have linked to any relevant issues.
- [ ] 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.
- [ ] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [ ] I have requested a review from the relevant team or maintainers.

---------

Co-authored-by: Sophie Dankel <47993817+sdankel@users.noreply.github.com>
Co-authored-by: K1-R1 <77465250+K1-R1@users.noreply.github.com>
  • Loading branch information
3 people committed Aug 14, 2024
1 parent 360fcf5 commit 9b87126
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 67 deletions.
3 changes: 2 additions & 1 deletion docs/book/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,5 @@ booleans
underflows
Codec
bool
str
str
multisig
10 changes: 10 additions & 0 deletions docs/book/src/forc/plugins/forc_client/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions forc-plugins/forc-client/src/cmd/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SecretKey>,
/// Sign the deployment transaction manually.
Expand Down
186 changes: 120 additions & 66 deletions forc-plugins/forc-client/src/op/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -52,7 +52,7 @@ pub struct DeploymentArtifact {
chain_id: ChainId,
contract_id: String,
deployment_size: usize,
deployed_block_height: u32,
deployed_block_height: Option<u32>,
}

impl DeploymentArtifact {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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::*;
Expand Down
41 changes: 41 additions & 0 deletions forc-plugins/forc-client/tests/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 9b87126

Please sign in to comment.