diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3d3d737d2f..6c2c103c31f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,8 +95,8 @@ jobs: cargo install --locked --debug --path ./forc-plugins/forc-fmt cargo install --locked --debug --path ./forc-plugins/forc-lsp cargo install --locked --debug --path ./forc-plugins/forc-client - cargo install --locked --debug --path ./forc-plugins/forc-tx cargo install --locked --debug --path ./forc-plugins/forc-doc + cargo install --locked --debug --path ./forc-plugins/forc-tx cargo install --locked --debug forc-explore - name: Install mdbook-forc-documenter run: cargo install --locked --debug --path ./scripts/mdbook-forc-documenter diff --git a/Cargo.lock b/Cargo.lock index b29ccd8f0cf..7f6e7b9e589 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1633,7 +1633,9 @@ version = "0.35.0" dependencies = [ "anyhow", "async-trait", + "chrono", "clap 3.2.23", + "devault", "forc", "forc-pkg", "forc-tracing", @@ -1649,6 +1651,7 @@ dependencies = [ "futures", "hex", "serde", + "serde_json", "sway-core", "sway-types", "sway-utils", diff --git a/docs/book/README.md b/docs/book/README.md index 80936783008..252eae7f122 100644 --- a/docs/book/README.md +++ b/docs/book/README.md @@ -21,10 +21,11 @@ cargo install --path ./scripts/mdbook-forc-documenter You must also install forc plugins that are already documented within the book. You can skip plugins that are going to be removed and install plugins that are going to be added to the book: ```sh +cargo install --path ./forc-plugins/forc-client +cargo install --path ./forc-plugins/forc-doc +cargo install forc-explore cargo install --path ./forc-plugins/forc-fmt cargo install --path ./forc-plugins/forc-lsp -cargo install forc-explore -cargo install --path ./forc-plugins/forc-doc ``` To build book: diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 5d7ad8154d1..5cd5284fb8e 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -76,6 +76,7 @@ - [forc client](./forc/plugins/forc_client/index.md) - [forc deploy](./forc/plugins/forc_client/forc_deploy.md) - [forc run](./forc/plugins/forc_client/forc_run.md) + - [forc submit](./forc/plugins/forc_client/forc_submit.md) - [forc doc](./forc/plugins/forc_doc.md) - [forc explore](./forc/plugins/forc_explore.md) - [forc fmt](./forc/plugins/forc_fmt.md) diff --git a/docs/book/src/forc/plugins/forc_client/forc_submit.md b/docs/book/src/forc/plugins/forc_client/forc_submit.md new file mode 100644 index 00000000000..ad314de43e7 --- /dev/null +++ b/docs/book/src/forc/plugins/forc_client/forc_submit.md @@ -0,0 +1 @@ +# forc submit diff --git a/forc-plugins/forc-client/Cargo.toml b/forc-plugins/forc-client/Cargo.toml index 70edda7adf4..a4d832e7acf 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -11,7 +11,9 @@ description = "A `forc` plugin for interacting with a Fuel node." [dependencies] anyhow = "1" async-trait = "0.1.58" +chrono = { version = "0.4", default-features = false, features = ["std"] } clap = { version = "3", features = ["derive", "env"] } +devault = "0.1" forc = { version = "0.35.0", path = "../../forc" } forc-pkg = { version = "0.35.0", path = "../../forc-pkg" } forc-tracing = { version = "0.35.0", path = "../../forc-tracing" } @@ -27,6 +29,7 @@ fuels-types = { workspace = true } futures = "0.3" hex = "0.4.3" serde = "1.0" +serde_json = "1" sway-core = { version = "0.35.0", path = "../../sway-core" } sway-types = { version = "0.35.0", path = "../../sway-types" } sway-utils = { version = "0.35.0", path = "../../sway-utils" } @@ -35,11 +38,15 @@ tracing = "0.1" [[bin]] name = "forc-deploy" -path = "src/bin/deploy/main.rs" +path = "src/bin/deploy.rs" [[bin]] name = "forc-run" -path = "src/bin/run/main.rs" +path = "src/bin/run.rs" + +[[bin]] +name = "forc-submit" +path = "src/bin/submit.rs" [lib] path = "src/lib.rs" diff --git a/forc-plugins/forc-client/src/bin/deploy/main.rs b/forc-plugins/forc-client/src/bin/deploy.rs similarity index 100% rename from forc-plugins/forc-client/src/bin/deploy/main.rs rename to forc-plugins/forc-client/src/bin/deploy.rs diff --git a/forc-plugins/forc-client/src/bin/run/main.rs b/forc-plugins/forc-client/src/bin/run.rs similarity index 100% rename from forc-plugins/forc-client/src/bin/run/main.rs rename to forc-plugins/forc-client/src/bin/run.rs diff --git a/forc-plugins/forc-client/src/bin/submit.rs b/forc-plugins/forc-client/src/bin/submit.rs new file mode 100644 index 00000000000..3561cf35297 --- /dev/null +++ b/forc-plugins/forc-client/src/bin/submit.rs @@ -0,0 +1,12 @@ +use clap::Parser; +use forc_tracing::init_tracing_subscriber; + +#[tokio::main] +async fn main() { + init_tracing_subscriber(Default::default()); + let command = forc_client::cmd::Submit::parse(); + if let Err(err) = forc_client::op::submit(command).await { + tracing::error!("Error: {:?}", err); + std::process::exit(1); + } +} diff --git a/forc-plugins/forc-client/src/cmd/deploy.rs b/forc-plugins/forc-client/src/cmd/deploy.rs index 01798c61726..d396e40bdaf 100644 --- a/forc-plugins/forc-client/src/cmd/deploy.rs +++ b/forc-plugins/forc-client/src/cmd/deploy.rs @@ -19,8 +19,9 @@ pub struct Command { pub build_output: BuildOutput, #[clap(flatten)] pub build_profile: BuildProfile, - /// The node url to deploy, if not specified uses DEFAULT_NODE_URL. - /// If url is specified overrides network url in manifest file (if there is one). + /// The URL of the Fuel node to which we're submitting the transaction. + /// If unspecified, checks the manifest's `network` table, then falls back + /// to [`crate::default::NODE_URL`]. #[clap(long, env = "FUEL_NODE_URL")] pub node_url: Option, /// Do not sign the transaction diff --git a/forc-plugins/forc-client/src/cmd/mod.rs b/forc-plugins/forc-client/src/cmd/mod.rs index 498bfd03a6c..8fd96dbc05c 100644 --- a/forc-plugins/forc-client/src/cmd/mod.rs +++ b/forc-plugins/forc-client/src/cmd/mod.rs @@ -1,5 +1,7 @@ pub mod deploy; pub mod run; +pub mod submit; pub use deploy::Command as Deploy; pub use run::Command as Run; +pub use submit::Command as Submit; diff --git a/forc-plugins/forc-client/src/cmd/run.rs b/forc-plugins/forc-client/src/cmd/run.rs index dd67addb28c..4ce2788b6fb 100644 --- a/forc-plugins/forc-client/src/cmd/run.rs +++ b/forc-plugins/forc-client/src/cmd/run.rs @@ -1,6 +1,7 @@ use clap::Parser; use fuel_crypto::SecretKey; +pub use super::submit::Network; pub use forc::cli::shared::{BuildOutput, BuildProfile, Minify, Pkg, Print}; pub use forc_tx::Gas; @@ -21,15 +22,17 @@ pub struct Command { pub build_output: BuildOutput, #[clap(flatten)] pub build_profile: BuildProfile, + /// The URL of the Fuel node to which we're submitting the transaction. + /// If unspecified, checks the manifest's `network` table, then falls back + /// to [`crate::default::NODE_URL`]. + #[clap(long, env = "FUEL_NODE_URL")] + pub node_url: Option, /// Hex string of data to input to script. #[clap(short, long)] pub data: Option, /// Only craft transaction and print it out. #[clap(long)] pub dry_run: bool, - /// URL of the Fuel Client Node - #[clap(long, env = "FUEL_NODE_URL")] - pub node_url: Option, /// Pretty-print the outputs from the node. #[clap(long = "pretty-print", short = 'r')] pub pretty_print: bool, diff --git a/forc-plugins/forc-client/src/cmd/submit.rs b/forc-plugins/forc-client/src/cmd/submit.rs new file mode 100644 index 00000000000..7a12d17a06f --- /dev/null +++ b/forc-plugins/forc-client/src/cmd/submit.rs @@ -0,0 +1,42 @@ +use devault::Devault; +use std::path::PathBuf; + +/// Submit a transaction to the specified fuel node. +#[derive(Debug, Default, clap::Parser)] +#[clap(about, version)] +pub struct Command { + #[clap(flatten)] + pub network: Network, + #[clap(flatten)] + pub tx_status: TxStatus, + /// Path to the Transaction that is to be submitted to the Fuel node. + /// + /// Paths to files ending with `.json` will be deserialized from JSON. + /// Paths to files ending with `.bin` will be deserialized from bytes + /// using the `fuel_tx::Transaction::try_from_bytes` constructor. + pub tx_path: PathBuf, +} + +/// Options related to networking. +#[derive(Debug, Devault, clap::Args)] +pub struct Network { + /// The URL of the Fuel node to which we're submitting the transaction. + #[clap(long, env = "FUEL_NODE_URL", default_value_t = String::from(crate::default::NODE_URL))] + #[devault("String::from(crate::default::NODE_URL)")] + pub node_url: String, + /// Whether or not to await confirmation that the transaction has been committed. + /// + /// When `true`, await commitment and output the transaction status. + /// When `false`, do not await confirmation and simply output the transaction ID. + #[clap(long = "await", default_value_t = true)] + #[devault("true")] + pub await_: bool, +} + +/// Options related to the transaction status. +#[derive(Debug, Default, clap::Args)] +pub struct TxStatus { + /// Output the resulting transaction status as JSON rather than the default output. + #[clap(long = "tx-status-json", default_value_t = false)] + pub json: bool, +} diff --git a/forc-plugins/forc-client/src/lib.rs b/forc-plugins/forc-client/src/lib.rs index cf610ef0acd..472d4c85ff7 100644 --- a/forc-plugins/forc-client/src/lib.rs +++ b/forc-plugins/forc-client/src/lib.rs @@ -1,3 +1,8 @@ pub mod cmd; pub mod op; mod util; + +pub mod default { + /// Default to localhost to favour the common case of testing. + pub const NODE_URL: &str = sway_utils::constants::DEFAULT_NODE_URL; +} diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index c5a7d111e47..8f2bcf442d8 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -17,7 +17,6 @@ use std::path::PathBuf; use std::time::Duration; use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; -use sway_utils::constants::DEFAULT_NODE_URL; use tracing::info; pub struct DeployedContract { @@ -57,12 +56,11 @@ pub async fn deploy_pkg( manifest: &PackageManifestFile, compiled: &BuiltPackage, ) -> Result { - let node_url = match &manifest.network { - Some(network) => &network.url, - _ => DEFAULT_NODE_URL, - }; - - let node_url = command.node_url.as_deref().unwrap_or(node_url); + 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.clone().into(); diff --git a/forc-plugins/forc-client/src/op/mod.rs b/forc-plugins/forc-client/src/op/mod.rs index 9e09aa18fcb..bb7e5746eb4 100644 --- a/forc-plugins/forc-client/src/op/mod.rs +++ b/forc-plugins/forc-client/src/op/mod.rs @@ -1,5 +1,7 @@ mod deploy; mod run; +mod submit; pub use deploy::deploy; pub use run::run; +pub use submit::submit; diff --git a/forc-plugins/forc-client/src/op/run.rs b/forc-plugins/forc-client/src/op/run.rs index e8df1ad1dc5..3c64406e707 100644 --- a/forc-plugins/forc-client/src/op/run.rs +++ b/forc-plugins/forc-client/src/op/run.rs @@ -19,8 +19,6 @@ use sway_core::BuildTarget; use tokio::time::timeout; use tracing::info; -pub const NODE_URL: &str = "http://127.0.0.1:4000"; - pub struct RanScript { pub receipts: Vec, } @@ -66,7 +64,7 @@ pub async fn run_pkg( .node_url .as_deref() .or_else(|| manifest.network.as_ref().map(|nw| &nw.url[..])) - .unwrap_or(NODE_URL); + .unwrap_or(crate::default::NODE_URL); let client = FuelClient::new(node_url)?; let contract_ids = command .contract diff --git a/forc-plugins/forc-client/src/op/submit.rs b/forc-plugins/forc-client/src/op/submit.rs new file mode 100644 index 00000000000..8f66f7df379 --- /dev/null +++ b/forc-plugins/forc-client/src/op/submit.rs @@ -0,0 +1,96 @@ +use crate::cmd; +use anyhow::Context; +use fuel_core_client::client::{types::TransactionStatus, FuelClient}; + +/// A command for submitting transactions to a Fuel network. +pub async fn submit(cmd: cmd::Submit) -> anyhow::Result<()> { + let tx = read_tx(&cmd.tx_path)?; + let client = FuelClient::new(&cmd.network.node_url)?; + if cmd.network.await_ { + let status = client + .submit_and_await_commit(&tx) + .await + .context("Submission of tx or awaiting commit failed")?; + if cmd.tx_status.json { + print_status_json(&status)?; + } else { + print_status(&status); + } + } else { + let id = client.submit(&tx).await.context("Failed to submit tx")?; + println!("{id}"); + } + Ok(()) +} + +/// Deserialize a `Transaction` from the given file into memory. +pub fn read_tx(path: &std::path::Path) -> anyhow::Result { + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + fn has_extension(path: &std::path::Path, ext: &str) -> bool { + path.extension().and_then(|ex| ex.to_str()) == Some(ext) + } + let tx: fuel_tx::Transaction = if has_extension(path, "json") { + serde_json::from_reader(reader)? + } else if has_extension(path, "bin") { + let tx_bytes = std::fs::read(path)?; + let (_bytes, tx) = fuel_tx::Transaction::try_from_bytes(&tx_bytes)?; + tx + } else { + anyhow::bail!(r#"Unsupported transaction file extension, expected ".json" or ".bin""#); + }; + Ok(tx) +} + +/// Format the transaction status in a more human-friendly manner. +pub fn fmt_status(status: &TransactionStatus, s: &mut String) -> anyhow::Result<()> { + use chrono::TimeZone; + use std::fmt::Write; + match status { + TransactionStatus::Submitted { submitted_at } => { + writeln!(s, "Transaction Submitted at {:?}", submitted_at.0)?; + } + TransactionStatus::Success { + block_id, + time, + program_state, + } => { + let utc = chrono::Utc.timestamp_nanos(time.to_unix()); + writeln!(s, "Transaction Succeeded")?; + writeln!(s, " Block ID: {block_id}")?; + writeln!(s, " Time: {utc}",)?; + writeln!(s, " Program State: {program_state:?}")?; + } + TransactionStatus::SqueezedOut { reason } => { + writeln!(s, "Transaction Squeezed Out: {reason}")?; + } + TransactionStatus::Failure { + block_id, + time, + reason, + program_state, + } => { + let utc = chrono::Utc.timestamp_nanos(time.to_unix()); + writeln!(s, "Transaction Failed")?; + writeln!(s, " Reason: {reason}")?; + writeln!(s, " Block ID: {block_id}")?; + writeln!(s, " Time: {utc}")?; + writeln!(s, " Program State: {program_state:?}")?; + } + } + Ok(()) +} + +/// Print the status to stdout. +pub fn print_status(status: &TransactionStatus) { + let mut string = String::new(); + fmt_status(status, &mut string).expect("formatting to `String` is infallible"); + println!("{string}"); +} + +/// Print the status to stdout in its JSON representation. +pub fn print_status_json(status: &TransactionStatus) -> anyhow::Result<()> { + let json = serde_json::to_string_pretty(status)?; + println!("{json}"); + Ok(()) +}