Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Candid assist #3546

Merged
merged 17 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

# 0.17.0

### feat: candid assist feature
ericswanson-dfinity marked this conversation as resolved.
Show resolved Hide resolved

Ask for user input when Candid argument is not provided in `dfx canister call`, `dfx canister install` and `dfx deploy`.
Previously, we cannot call `dfx deploy --all` when multiple canisters require init args. With the Candid assist feature,
dfx now asks for init args in terminal when a canister requires init args.

### fix!: always fetch did file from canister when making canister calls

`dfx canister call` will always fetch did file from the canister metadata. This is especially helpful for calling remote canisters. It's a breaking change in the sense that if the canister doesn't have the `candid:service` metadata, we will not read the local did file from build artifact, and dfx will issue a warning in this case to encourage canister developers to put the did file into canister metadata.
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions src/dfx-core/src/config/model/canister_id_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,37 @@ impl CanisterIdStore {
.or_else(|| self.find_in(canister_name, &self.ids))
.or_else(|| self.pull_ids.get(canister_name).copied())
}
pub fn get_name_id_map(&self) -> BTreeMap<String, String> {
let mut ids: BTreeMap<_, _> = self
.ids
.iter()
.filter_map(|(name, network_to_id)| {
Some((
name.clone(),
network_to_id.get(&self.network_descriptor.name).cloned()?,
))
})
.collect();
if let Some(remote_ids) = &self.remote_ids {
let mut remote = remote_ids
.iter()
.filter_map(|(name, network_to_id)| {
Some((
name.clone(),
network_to_id.get(&self.network_descriptor.name).cloned()?,
))
})
.collect();
ids.append(&mut remote);
}
let mut pull_ids = self
.pull_ids
.iter()
.map(|(name, id)| (name.clone(), id.to_text()))
.collect();
ids.append(&mut pull_ids);
ids
}

fn find_in(&self, canister_name: &str, canister_ids: &CanisterIds) -> Option<CanisterId> {
canister_ids
Expand Down
6 changes: 5 additions & 1 deletion src/dfx-core/src/identity/identity_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,11 @@ impl IdentityManager {
Ok(identity)
}

fn load_identity(&self, name: &str, log: &Logger) -> Result<DfxIdentity, LoadIdentityError> {
pub fn load_identity(
&self,
name: &str,
log: &Logger,
) -> Result<DfxIdentity, LoadIdentityError> {
let config = self
.get_identity_config_or_default(name)
.map_err(LoadIdentityError::GetIdentityConfigOrDefaultFailed)?;
Expand Down
2 changes: 1 addition & 1 deletion src/dfx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ base64.workspace = true
byte-unit = { workspace = true, features = ["serde"] }
bytes.workspace = true
candid = { workspace = true }
candid_parser = { workspace = true, features = ["random"] }
candid_parser = { workspace = true, features = ["random", "assist"] }
clap = { workspace = true, features = [
"derive",
"env",
Expand Down
8 changes: 7 additions & 1 deletion src/dfx/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,13 @@ pub async fn exec(

// Get the argument, get the type, convert the argument to the type and return
// an error if any of it doesn't work.
let arg_value = blob_from_arguments(arguments, opts.random.as_deref(), arg_type, &method_type)?;
let arg_value = blob_from_arguments(
Some(env),
arguments,
opts.random.as_deref(),
arg_type,
&method_type,
)?;

// amount has been validated by cycle_amount_validator
let cycles = opts.with_cycles.unwrap_or(0);
Expand Down
2 changes: 1 addition & 1 deletion src/dfx/src/commands/canister/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ async fn delete_canister(
"Installing temporary wallet in canister {} to enable transfer of cycles.",
canister
);
let args = blob_from_arguments(None, None, None, &None)?;
let args = blob_from_arguments(None, None, None, None, &None)?;
let mode = InstallMode::Reinstall;
let install_builder = mgr
.install_code(&canister_id, &wasm_module)
Expand Down
7 changes: 4 additions & 3 deletions src/dfx/src/commands/canister/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ pub async fn exec(
if let Some(wasm_path) = opts.wasm {
// streamlined version, we can ignore most of the environment
let mode = mode.context("The install mode cannot be auto when using --wasm")?;
let install_args = || blob_from_arguments(arguments, None, arg_type, &None);
let install_args = || blob_from_arguments(Some(env), arguments, None, arg_type, &None);
install_canister(
env,
&mut canister_id_store,
Expand All @@ -158,7 +158,8 @@ pub async fn exec(
let env_file = config.get_output_env_file(opts.output_env_file)?;
let idl_path = canister_info.get_constructor_idl_path();
let init_type = get_candid_init_type(&idl_path);
let install_args = || blob_from_arguments(arguments, None, arg_type, &init_type);
let install_args =
|| blob_from_arguments(Some(env), arguments, None, arg_type, &init_type);
install_canister(
env,
&mut canister_id_store,
Expand Down Expand Up @@ -205,7 +206,7 @@ pub async fn exec(

let idl_path = canister_info.get_constructor_idl_path();
let init_type = get_candid_init_type(&idl_path);
let install_args = || blob_from_arguments(None, None, None, &init_type);
let install_args = || blob_from_arguments(Some(env), None, None, None, &init_type);

install_canister(
env,
Expand Down
8 changes: 7 additions & 1 deletion src/dfx/src/commands/canister/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,13 @@ pub async fn exec(

// Get the argument, get the type, convert the argument to the type and return
// an error if any of it doesn't work.
let arg_value = blob_from_arguments(arguments, opts.random.as_deref(), arg_type, &method_type)?;
let arg_value = blob_from_arguments(
Some(env),
arguments,
opts.random.as_deref(),
arg_type,
&method_type,
)?;
let agent = env.get_agent();

let network = env
Expand Down
2 changes: 1 addition & 1 deletion src/dfx/src/lib/integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub async fn initialize_integration_canister(
};
try_create_canister(agent, logger, &canister_id, &pulled_canister).await?;

let install_arg = blob_from_arguments(Some(init_arg), None, None, &None)?;
let install_arg = blob_from_arguments(None, Some(init_arg), None, None, &None)?;
install_canister(agent, logger, &canister_id, wasm, install_arg, name).await
}

Expand Down
3 changes: 2 additions & 1 deletion src/dfx/src/lib/operations/canister/deploy_canisters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ async fn install_canisters(

let idl_path = canister_info.get_constructor_idl_path();
let init_type = get_candid_init_type(&idl_path);
let install_args = || blob_from_arguments(argument, None, argument_type, &init_type);
let install_args =
|| blob_from_arguments(Some(env), argument, None, argument_type, &init_type);

install_canister(
env,
Expand Down
51 changes: 50 additions & 1 deletion src/dfx/src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::lib::environment::Environment;
use crate::lib::error::DfxResult;
use crate::{error_invalid_argument, error_invalid_data, error_unknown};
use anyhow::{bail, Context};
Expand All @@ -16,7 +17,8 @@ use net2::{TcpBuilder, TcpListenerExt};
use num_traits::FromPrimitive;
use reqwest::{Client, StatusCode, Url};
use rust_decimal::Decimal;
use std::io::{stdin, Read};
use std::collections::BTreeMap;
use std::io::{stdin, stdout, IsTerminal, Read};
use std::net::{IpAddr, SocketAddr};
use std::path::Path;
use std::time::Duration;
Expand Down Expand Up @@ -149,6 +151,7 @@ pub fn arguments_from_file(file_name: &Path) -> DfxResult<String> {

#[context("Failed to create argument blob.")]
pub fn blob_from_arguments(
dfx_env: Option<&dyn Environment>,
arguments: Option<&str>,
random: Option<&str>,
arg_type: Option<&str>,
Expand All @@ -172,6 +175,7 @@ pub fn blob_from_arguments(
.to_bytes()
}
Some((env, func)) => {
let is_terminal = stdin().is_terminal() && stdout().is_terminal();
if let Some(arguments) = arguments {
fuzzy_parse_argument(arguments, env, &func.args)
} else if func.args.is_empty() {
Expand All @@ -181,6 +185,7 @@ pub fn blob_from_arguments(
.args
.iter()
.all(|t| matches!(t.as_ref(), TypeInner::Opt(_)))
&& !is_terminal
{
// If the user provided no arguments, and if all the expected arguments are
// optional, then use null values.
Expand All @@ -203,6 +208,26 @@ pub fn blob_from_arguments(
.context("Failed to create idl args.")?;
eprintln!("Sending the following random argument:\n{}\n", args);
args.to_bytes_with_types(env, &func.args)
} else if is_terminal {
use candid_parser::assist::{input_args, Context};
let mut ctx = Context::new(env.clone());
if let Some(env) = dfx_env {
let principals = gather_principals_from_env(env);
if !principals.is_empty() {
let mut map = BTreeMap::new();
map.insert("principal".to_string(), principals);
ctx.set_completion(map);
ericswanson-dfinity marked this conversation as resolved.
Show resolved Hide resolved

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This set_completion call overwrites the completions, which makes it so anonymous doesn't show up in the list of autocompletion principals. Is that intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's intended. We probably need to add anonymous manually in the identity export.

}
}
let args = input_args(&ctx, &func.args)?;
eprintln!("Sending the following argument:\n{}\n", args);
eprintln!("Do you want to send this message? [y/N]");
let mut input = String::new();
stdin().read_line(&mut input)?;
if !["y", "Y", "yes", "Yes", "YES"].contains(&input.trim()) {
return Err(error_invalid_data!("User cancelled."));
}
args.to_bytes_with_types(env, &func.args)
} else {
return Err(error_invalid_data!("Expected arguments but found none."));
}
Expand All @@ -215,6 +240,30 @@ pub fn blob_from_arguments(
}
}

pub fn gather_principals_from_env(env: &dyn Environment) -> BTreeMap<String, String> {
use ic_agent::Identity;
let mut res: BTreeMap<String, String> = BTreeMap::new();
if let Ok(mgr) = env.new_identity_manager() {
let logger = env.get_logger();
if let Ok(names) = mgr.get_identity_names(logger) {
let mut map = names
.iter()
.filter_map(|name| {
let id = mgr.load_identity(name, logger).ok()?;
chenyan-dfinity marked this conversation as resolved.
Show resolved Hide resolved
let sender = id.sender().ok()?;
Some((name.clone(), sender.to_text()))
})
.collect();
res.append(&mut map);
}
}
if let Ok(canisters) = env.get_canister_id_store() {
let mut canisters = canisters.get_name_id_map();
res.append(&mut canisters);
}
res
}

pub fn fuzzy_parse_argument(
arg_str: &str,
env: &TypeEnv,
Expand Down
Loading