diff --git a/CHANGELOG.md b/CHANGELOG.md index 21d41b28fa..7f2d752ab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ If you want to disable this behavior, you can config it in dfx.json: } } +### feat: Add dfx sns deploy + +This allows users to deploy a set of SNS canisters. + ### fix: `cargo run -p dfx -- --version` prints correct version ### feat: add --mode=auto diff --git a/docs/cli-reference/dfx-sns.md b/docs/cli-reference/dfx-sns.md index 6e9561c6f1..26893c3eb3 100644 --- a/docs/cli-reference/dfx-sns.md +++ b/docs/cli-reference/dfx-sns.md @@ -11,8 +11,9 @@ Depending on the `dfx sns` subcommand you specify, additional arguments, options | Command | Description | |-------------------------------------|-------------------------------------------------------------------------------| -| [`create`](#_dfx_sns_create) | Creates an SNS configuration template | +| [`create`](#_dfx_sns_create) | Creates an SNS configuration template. | | [`validate`](#_dfx_sns_validate) | Checks whether the sns config file is valid. | +| [`deploy`](#_dfx_sns_deploy) | Deploys SNS canisters according to the local config. | | `help` | Displays usage information message for a specified subcommand. | To view usage information for a specific subcommand, specify the subcommand and the `--help` flag. For example, to see usage information for `dfx sns validate`, you can run the following command: @@ -74,6 +75,48 @@ You can use the following optional flags with the `dfx sns validate` command. You can use the `dfx sns validate` command to verify that a configuration template is valid. It is not; it needs details such as token name: ``` bash -dfx sns create -dfx sns validate +dfx sns config create +``` +Fill in the blank fields, then: +``` bash +dfx sns config validate +``` + +## dfx sns deploy + +Use the `dfx sns deploy` command to create SNS canisters according to the local configuration file. + +Note: Deploying SNS canisters does not require a proposal, however there is a hefty fee. Please don't create canisters on mainnet until you have tested your configuration locally and are sure that you are happy with it. + +### Basic usage + +``` bash +dfx sns deploy +``` + +### Flags + +You can use the following optional flags with the `dfx sns deploy` command. + +| Flag | Description | +|-------------------|-------------------------------| +| `-h`, `--help` | Displays usage information. | +| `-V`, `--version` | Displays version information. | + +### Examples + +Create an SNS on the local testnet: +``` bash +dfx sns config create +``` +Fill in the blank fields, then: +``` bash +dfx sns config validate +dfx sns deploy ``` +You can now verify that the sns canisters have been created. E.g.: +``` +dfx canister info sns_root +dfx canister info sns_ledger +``` + diff --git a/e2e/assets/sns/logo.svg b/e2e/assets/sns/valid/logo.svg similarity index 100% rename from e2e/assets/sns/logo.svg rename to e2e/assets/sns/valid/logo.svg diff --git a/e2e/assets/sns/valid_sns_init_config.yaml b/e2e/assets/sns/valid/sns.yml similarity index 100% rename from e2e/assets/sns/valid_sns_init_config.yaml rename to e2e/assets/sns/valid/sns.yml diff --git a/e2e/tests-dfx/sns.bash b/e2e/tests-dfx/sns.bash index 5f37779fbf..08417e8228 100755 --- a/e2e/tests-dfx/sns.bash +++ b/e2e/tests-dfx/sns.bash @@ -34,16 +34,46 @@ SNS_CONFIG_FILE_NAME="sns.yml" @test "sns config validate approves a valid configuration" { dfx_new - cp "${BATS_TEST_DIRNAME}/../assets/sns/valid_sns_init_config.yaml" "$SNS_CONFIG_FILE_NAME" - cp "${BATS_TEST_DIRNAME}/../assets/sns/logo.svg" . + install_asset sns/valid assert_command dfx sns config validate assert_match 'SNS config file is valid' } @test "sns config validate identifies a missing key" { dfx_new - grep -v token_name "${BATS_TEST_DIRNAME}/../assets/sns/valid_sns_init_config.yaml" > "$SNS_CONFIG_FILE_NAME" - cp "${BATS_TEST_DIRNAME}/../assets/sns/logo.svg" . + install_asset sns/valid + grep -v token_name "${SNS_CONFIG_FILE_NAME}" | sponge "$SNS_CONFIG_FILE_NAME" assert_command_fail dfx sns config validate assert_match token.name } + +@test "sns deploy exists" { + dfx sns deploy --help +} + +@test "sns deploy fails without config file" { + dfx_new + rm -f sns.yml # Is not expected to be present anyway + assert_command_fail dfx sns deploy + assert_match "Error encountered when generating the SnsInitPayload: Couldn't open initial parameters file" +} + +@test "sns deploy succeeds" { + dfx_new + install_shared_asset subnet_type/shared_network_settings/system + dfx start --clean --background --host 127.0.0.1:8080 + sleep 1 + dfx nns install + # There are no entries for "local" upstream yet, so we need a network mapping. + dfx nns import --network-mapping local=mainnet + # This canister ID is not included upstream .. yet. + jq '.canisters["nns-sns-wasm"].remote.id.local="qaa6y-5yaaa-aaaaa-aaafa-cai"' dfx.json | sponge dfx.json + ls candid + cat dfx.json + dfx nns import --network-mapping local + ls candid + cat dfx.json + install_asset sns/valid + dfx sns config validate + dfx sns deploy +} diff --git a/src/dfx/src/commands/nns/import.rs b/src/dfx/src/commands/nns/import.rs index cb966381cb..6cb203cc09 100644 --- a/src/dfx/src/commands/nns/import.rs +++ b/src/dfx/src/commands/nns/import.rs @@ -1,3 +1,4 @@ +//! Code for the command line: `dfx nns import` use crate::lib::error::DfxResult; use crate::lib::info::replica_rev; use crate::lib::project::import::import_canister_definitions; @@ -19,6 +20,7 @@ pub struct ImportOpts { network_mapping: Vec, } +/// Executes `dfx nns import` pub async fn exec(env: &dyn Environment, opts: ImportOpts) -> DfxResult { let config = env.get_config_or_anyhow()?; let mut config = config.as_ref().clone(); diff --git a/src/dfx/src/commands/nns/install.rs b/src/dfx/src/commands/nns/install.rs index a31dbc839b..49d91fd4a9 100644 --- a/src/dfx/src/commands/nns/install.rs +++ b/src/dfx/src/commands/nns/install.rs @@ -1,3 +1,4 @@ +//! Code for the command line: `dfx nns install` use crate::lib::error::DfxResult; use crate::Environment; use anyhow::anyhow; @@ -23,6 +24,7 @@ use clap::Parser; #[clap(about)] pub struct InstallOpts {} +/// Executes `dfx nns install`. pub async fn exec(env: &dyn Environment, _opts: InstallOpts) -> DfxResult { let agent = env .get_agent() diff --git a/src/dfx/src/commands/nns/mod.rs b/src/dfx/src/commands/nns/mod.rs index dcde4d7556..90685a8197 100644 --- a/src/dfx/src/commands/nns/mod.rs +++ b/src/dfx/src/commands/nns/mod.rs @@ -1,3 +1,5 @@ +//! Code for the command line `dfx nns`. +#![warn(clippy::missing_docs_in_private_items)] use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::provider::create_agent_environment; @@ -9,25 +11,31 @@ use tokio::runtime::Runtime; mod import; mod install; -/// NNS commands. +/// Options for `dfx nns` and its subcommands. #[derive(Parser)] #[clap(name("nns"))] pub struct NnsOpts { + /// `dfx nns` subcommand arguments. #[clap(subcommand)] subcmd: SubCommand, + /// An argument to choose the network from those specified in dfx.json. #[clap(flatten)] network: NetworkOpt, } +/// Command line options for subcommands of `dfx nns`. #[derive(Parser)] enum SubCommand { + /// Options for importing NNS API definitions and canister IDs. #[clap(hide(true))] Import(import::ImportOpts), + /// Options for installing an NNS. #[clap(hide(true))] Install(install::InstallOpts), } +/// Executes `dfx nns` and its subcommands. pub fn exec(env: &dyn Environment, opts: NnsOpts) -> DfxResult { let env = create_agent_environment(env, opts.network.network)?; let runtime = Runtime::new().expect("Unable to create a runtime"); diff --git a/src/dfx/src/commands/sns/config/create.rs b/src/dfx/src/commands/sns/config/create.rs index 1e6007c4d7..41cc59dbe2 100644 --- a/src/dfx/src/commands/sns/config/create.rs +++ b/src/dfx/src/commands/sns/config/create.rs @@ -1,3 +1,4 @@ +//! Code for executing `dfx sns config create` use crate::lib::error::DfxResult; use crate::Environment; @@ -9,6 +10,7 @@ use clap::Parser; #[derive(Parser)] pub struct CreateOpts {} +/// Executes `dfx sns config create` pub fn exec(env: &dyn Environment, _opts: CreateOpts) -> DfxResult { let config = env.get_config_or_anyhow()?; let path = config.get_project_root().join(sns::CONFIG_FILE_NAME); diff --git a/src/dfx/src/commands/sns/config/mod.rs b/src/dfx/src/commands/sns/config/mod.rs index 05734a9d3c..d49c7019b8 100644 --- a/src/dfx/src/commands/sns/config/mod.rs +++ b/src/dfx/src/commands/sns/config/mod.rs @@ -1,26 +1,33 @@ +//! Code for the command line `dfx sns config`. use crate::lib::{environment::Environment, error::DfxResult}; use clap::Parser; mod create; mod validate; +/// Command line options for `sdx sns config`. #[derive(Parser)] pub struct ConfigOpts {} -/// SNS config commands. +/// SNS config command line arguments. #[derive(Parser)] #[clap(name("config"))] pub struct SnsConfigOpts { + /// `dfx sns config` subcommand arguments. #[clap(subcommand)] subcmd: SubCommand, } +/// Command line options for `sdx sns` subcommands. #[derive(Parser)] enum SubCommand { + /// Command line options for creating an SNS configuration. Create(create::CreateOpts), + /// Command line options for validating an SNS configuration. Validate(validate::ValidateOpts), } +/// Executes `dfx sns config` and its subcommands. pub fn exec(env: &dyn Environment, opts: SnsConfigOpts) -> DfxResult { match opts.subcmd { SubCommand::Create(v) => create::exec(env, v), diff --git a/src/dfx/src/commands/sns/config/validate.rs b/src/dfx/src/commands/sns/config/validate.rs index 8466fb99a2..23e587e409 100644 --- a/src/dfx/src/commands/sns/config/validate.rs +++ b/src/dfx/src/commands/sns/config/validate.rs @@ -1,3 +1,4 @@ +//! Code for executing `dfx sns config validate` use crate::lib::error::DfxResult; use crate::Environment; @@ -9,6 +10,7 @@ use clap::Parser; #[derive(Parser)] pub struct ValidateOpts {} +/// Executes `dfx sns config validate` pub fn exec(env: &dyn Environment, _opts: ValidateOpts) -> DfxResult { let config = env.get_config_or_anyhow()?; let path = config.get_project_root().join(sns::CONFIG_FILE_NAME); diff --git a/src/dfx/src/commands/sns/deploy.rs b/src/dfx/src/commands/sns/deploy.rs new file mode 100644 index 0000000000..3fd5764864 --- /dev/null +++ b/src/dfx/src/commands/sns/deploy.rs @@ -0,0 +1,24 @@ +//! Code for the command line `dfx sns deploy`. +use crate::lib::error::DfxResult; +use crate::Environment; + +use crate::lib::sns; +use crate::lib::sns::deploy::deploy_sns; +use clap::Parser; + +/// Creates an SNS on a network. +/// +/// # Arguments +/// - `env` - The execution environment, including the network to deploy to and connection credentials. +/// - `opts` - Deployment options. +#[derive(Parser)] +pub struct DeployOpts {} + +/// Executes the command line `dfx sns deploy`. +pub fn exec(env: &dyn Environment, _opts: DeployOpts) -> DfxResult { + let config = env.get_config_or_anyhow()?; + let path = config.get_project_root().join(sns::CONFIG_FILE_NAME); + + deploy_sns(env, &path)?; + Ok(()) +} diff --git a/src/dfx/src/commands/sns/import.rs b/src/dfx/src/commands/sns/import.rs index 7e5d60deb7..cc50c761fd 100644 --- a/src/dfx/src/commands/sns/import.rs +++ b/src/dfx/src/commands/sns/import.rs @@ -1,3 +1,4 @@ +//! Code for the command line `dfx sns import` use crate::lib::error::DfxResult; use crate::lib::project::import::import_canister_definitions; use crate::lib::project::network_mappings::get_network_mappings; @@ -19,6 +20,7 @@ pub struct SnsImportOpts { network_mapping: Vec, } +/// Executes the command line `dfx sns import`. pub fn exec(env: &dyn Environment, opts: SnsImportOpts) -> DfxResult { let config = env.get_config_or_anyhow()?; let mut config = config.as_ref().clone(); diff --git a/src/dfx/src/commands/sns/mod.rs b/src/dfx/src/commands/sns/mod.rs index d29e93a149..6db40fd4fb 100644 --- a/src/dfx/src/commands/sns/mod.rs +++ b/src/dfx/src/commands/sns/mod.rs @@ -1,3 +1,5 @@ +//! Command line interface for `dfx sns`. +#![warn(clippy::missing_docs_in_private_items)] use crate::{ commands::sns::config::SnsConfigOpts, commands::sns::import::SnsImportOpts, @@ -7,27 +9,37 @@ use crate::{ use clap::Parser; mod config; +mod deploy; mod import; -/// SNS commands. +/// Options for `dfx sns`. #[derive(Parser)] #[clap(name("sns"))] pub struct SnsOpts { + /// Arguments and flags for subcommands. #[clap(subcommand)] subcmd: SubCommand, } +/// Subcommands of `dfx sns` #[derive(Parser)] enum SubCommand { + /// Subcommands for working with configuration. #[clap(hide(true))] Config(SnsConfigOpts), + /// Subcommand for creating an SNS. + #[clap(hide(true))] + Deploy(deploy::DeployOpts), + /// Subcommand for importing sns API definitions and canister IDs. #[clap(hide(true))] Import(SnsImportOpts), } +/// Executes `dfx sns` and its subcommands. pub fn exec(env: &dyn Environment, cmd: SnsOpts) -> DfxResult { match cmd.subcmd { SubCommand::Config(v) => config::exec(env, v), SubCommand::Import(v) => import::exec(env, v), + SubCommand::Deploy(v) => deploy::exec(env, v), } } diff --git a/src/dfx/src/lib/nns/install_nns.rs b/src/dfx/src/lib/nns/install_nns.rs index a16ce73ba5..59b0f3cdd2 100644 --- a/src/dfx/src/lib/nns/install_nns.rs +++ b/src/dfx/src/lib/nns/install_nns.rs @@ -179,6 +179,9 @@ async fn get_subnet_id(agent: &Agent) -> anyhow::Result { /// The NNS canisters use the very first few canister IDs; they must be available. #[context("Failed to verify that the network is empty; dfx nns install must be run just after dfx start --clean")] async fn verify_nns_canister_ids_are_available(agent: &Agent) -> anyhow::Result<()> { + /// Checks that the canister is unused on the given network. + /// + /// The network is queried directly; local state such as canister_ids.json has no effect on this function. async fn verify_canister_id_is_available( agent: &Agent, canister_id: &str, diff --git a/src/dfx/src/lib/nns/mod.rs b/src/dfx/src/lib/nns/mod.rs index e9f0c68428..9a1f79d65c 100644 --- a/src/dfx/src/lib/nns/mod.rs +++ b/src/dfx/src/lib/nns/mod.rs @@ -1 +1,3 @@ +//! Code for managing the network nervous system. +#![warn(clippy::missing_docs_in_private_items)] pub mod install_nns; diff --git a/src/dfx/src/lib/sns/create_config.rs b/src/dfx/src/lib/sns/create_config.rs index f2f0fb07c7..b9e5488479 100644 --- a/src/dfx/src/lib/sns/create_config.rs +++ b/src/dfx/src/lib/sns/create_config.rs @@ -1,3 +1,4 @@ +//! Code for creating SNS configurations use anyhow::{anyhow, Context}; use fn_error_context::context; use std::path::Path; @@ -6,6 +7,7 @@ use std::process::{self, Command}; use crate::lib::error::DfxResult; use crate::Environment; +/// Ceates an SNS configuration template. #[context("Failed to create sns config at {}.", path.display())] pub fn create_config(env: &dyn Environment, path: &Path) -> DfxResult { let cli_name = "sns"; diff --git a/src/dfx/src/lib/sns/deploy.rs b/src/dfx/src/lib/sns/deploy.rs new file mode 100644 index 0000000000..dc251b416d --- /dev/null +++ b/src/dfx/src/lib/sns/deploy.rs @@ -0,0 +1,19 @@ +//! Code for creating an SNS. +use fn_error_context::context; +use std::ffi::OsString; +use std::path::Path; + +use crate::lib::error::DfxResult; +use crate::lib::sns::sns_cli::call_sns_cli; +use crate::Environment; + +/// Creates an SNS. This requires funds but no proposal. +#[context("Failed to deploy SNS with config: {}", path.display())] +pub fn deploy_sns(env: &dyn Environment, path: &Path) -> DfxResult { + let args = vec![ + OsString::from("deploy"), + OsString::from("--init-config-file"), + OsString::from(path), + ]; + call_sns_cli(env, &args).map(|stdout| format!("Deployed SNS: {}\n{}", path.display(), stdout)) +} diff --git a/src/dfx/src/lib/sns/mod.rs b/src/dfx/src/lib/sns/mod.rs index 10411ac620..1a94f02831 100644 --- a/src/dfx/src/lib/sns/mod.rs +++ b/src/dfx/src/lib/sns/mod.rs @@ -1,5 +1,9 @@ +//! Code for decentralizing dapps +#![warn(clippy::missing_docs_in_private_items)] pub mod create_config; +pub mod deploy; pub mod sns_cli; pub mod validate_config; +/// The default location of an SNS configuration file. pub const CONFIG_FILE_NAME: &str = "sns.yml"; diff --git a/src/dfx/src/lib/sns/sns_cli.rs b/src/dfx/src/lib/sns/sns_cli.rs index 547376f8a8..1e79c40a59 100644 --- a/src/dfx/src/lib/sns/sns_cli.rs +++ b/src/dfx/src/lib/sns/sns_cli.rs @@ -1,12 +1,14 @@ +//! Library for calling the `sns` command line tool. use anyhow::{anyhow, Context}; use fn_error_context::context; use std::ffi::OsStr; +use std::path::Path; use std::process::{self, Command}; use crate::lib::error::DfxResult; use crate::Environment; -/// Calls the sns cli tool from the SNS codebase in the ic repo. +/// Calls the sns command line tool from the SNS codebase in the ic repo. #[context("Failed to call sns CLI.")] pub fn call_sns_cli(env: &dyn Environment, args: I) -> DfxResult where @@ -18,8 +20,11 @@ where .get_cache() .get_binary_command_path(cli_name) .with_context(|| format!("Could not find bundled binary '{cli_name}'."))?; - let mut command = Command::new(sns_cli); + let mut command = Command::new(&sns_cli); command.args(args); + // The sns command line tool itself calls dfx; it should call this dfx. + // The sns command line tool should not rely on commands not packaged with dfx. + command.env("PATH", sns_cli.parent().unwrap_or_else(|| Path::new("."))); command .stdin(process::Stdio::null()) .output() @@ -28,10 +33,15 @@ where if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).into_owned()) } else { + let args: Vec<_> = command + .get_args() + .into_iter() + .map(OsStr::to_string_lossy) + .collect(); Err(anyhow!( - "SNS cli call failed:\n{:?} {:?}\nStdout:\n{:?}\n\nStderr:\n{:?}", + "SNS cli call failed:\n{:?} {}\nStdout:\n{}\n\nStderr:\n{}", command.get_program(), - command.get_args(), + args.join(" "), String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) )) diff --git a/src/dfx/src/lib/sns/validate_config.rs b/src/dfx/src/lib/sns/validate_config.rs index 2e91c41d18..a8d5d8b8d7 100644 --- a/src/dfx/src/lib/sns/validate_config.rs +++ b/src/dfx/src/lib/sns/validate_config.rs @@ -1,3 +1,4 @@ +//! Code for checking SNS config file validity use fn_error_context::context; use std::ffi::OsString; use std::path::Path; @@ -6,7 +7,7 @@ use crate::lib::error::DfxResult; use crate::lib::sns::sns_cli::call_sns_cli; use crate::Environment; -/// +/// Checks whether an SNS configuration file is valid. #[context("Failed to validate SNS config at {}.", path.display())] pub fn validate_config(env: &dyn Environment, path: &Path) -> DfxResult { let args = vec![