Skip to content

Commit

Permalink
feat: fetch did file from canister
Browse files Browse the repository at this point in the history
  • Loading branch information
chenyan-dfinity committed Jan 31, 2024
1 parent 3db15d4 commit 28dc1e4
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 55 deletions.
38 changes: 30 additions & 8 deletions src/dfx/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ use crate::lib::error::DfxResult;
use crate::lib::operations::canister::get_local_cid_and_candid_path;
use crate::lib::root_key::fetch_root_key_if_needed;
use crate::util::clap::parsers::{cycle_amount_parser, file_or_stdin_parser};
use crate::util::{arguments_from_file, blob_from_arguments, get_candid_type, print_idl_blob};
use crate::util::{
arguments_from_file, blob_from_arguments, fetch_remote_did_file, get_candid_type,
print_idl_blob,
};
use anyhow::{anyhow, Context};
use candid::Principal as CanisterId;
use candid::{CandidType, Decode, Deserialize, Principal};
use candid_parser::utils::CandidSource;
use clap::Parser;
use dfx_core::canister::build_wallet_canister;
use dfx_core::identity::CallSender;
Expand Down Expand Up @@ -218,16 +222,18 @@ pub async fn exec(
opts: CanisterCallOpts,
call_sender: &CallSender,
) -> DfxResult {
let agent = env.get_agent();
fetch_root_key_if_needed(env).await?;

let callee_canister = opts.canister_name.as_str();
let method_name = opts.method_name.as_str();
let canister_id_store = env.get_canister_id_store()?;

let (canister_id, maybe_candid_path) = match CanisterId::from_text(callee_canister) {
let (canister_id, maybe_local_candid_path) = match CanisterId::from_text(callee_canister) {
Ok(id) => {
if let Some(canister_name) = canister_id_store.get_name(callee_canister) {
get_local_cid_and_candid_path(env, canister_name, Some(id))?
} else {
// TODO fetch candid file from remote canister
(id, None)
}
}
Expand All @@ -236,11 +242,30 @@ pub async fn exec(
get_local_cid_and_candid_path(env, callee_canister, Some(canister_id))?
}
};
let maybe_candid_path = opts.candid.or(maybe_candid_path);
let method_type = if let Some(path) = opts.candid {
get_candid_type(CandidSource::File(&path), method_name)
} else if let Some(did) = fetch_remote_did_file(agent, canister_id).await {
get_candid_type(CandidSource::Text(&did), method_name)
} else if let Some(path) = maybe_local_candid_path {
eprintln!("DEPRECATION WARNING: Cannot fetch Candid interface from canister metadata, reading Candid interface from the local build artifact. In a future dfx release, we will only read candid interface from canister metadata.");
eprintln!(
r#"Please add the following to dfx.json to store local candid file into metadata:
"metadata": [
{{
"name": "candid:service"
}}
]"#
);
get_candid_type(CandidSource::File(&path), method_name)
} else {
None
};
if method_type.is_none() {
eprintln!("Cannot fetch Candid interface from canister metadata, sending arguments with inferred types.");
}

let is_management_canister = canister_id == CanisterId::management_canister();

let method_type = maybe_candid_path.and_then(|path| get_candid_type(&path, method_name));
let is_query_method = method_type.as_ref().map(|(_, f)| f.is_query());

let arguments_from_file = opts
Expand Down Expand Up @@ -275,9 +300,6 @@ 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 agent = env.get_agent();

fetch_root_key_if_needed(env).await?;

// amount has been validated by cycle_amount_validator
let cycles = opts.with_cycles.unwrap_or(0);
Expand Down
6 changes: 4 additions & 2 deletions src/dfx/src/commands/canister/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::util::clap::parsers::file_or_stdin_parser;
use crate::util::{arguments_from_file, blob_from_arguments, get_candid_type};
use anyhow::{anyhow, bail, Context};
use candid::Principal;
use candid_parser::utils::CandidSource;
use clap::Parser;
use dfx_core::identity::CallSender;
use ic_agent::AgentError;
Expand Down Expand Up @@ -86,7 +87,7 @@ pub async fn exec(
if let Some(canister_name) = canister_id_store.get_name(callee_canister) {
get_local_cid_and_candid_path(env, canister_name, Some(id))?
} else {
// TODO fetch candid file from remote canister
// Sign works in offline mode, cannot fetch from remote canister
(id, None)
}
}
Expand All @@ -96,7 +97,8 @@ pub async fn exec(
}
};

let method_type = maybe_candid_path.and_then(|path| get_candid_type(&path, method_name));
let method_type =
maybe_candid_path.and_then(|path| get_candid_type(CandidSource::File(&path), method_name));
let is_query_method = method_type.as_ref().map(|(_, f)| f.is_query());

let arguments_from_file = opts
Expand Down
6 changes: 3 additions & 3 deletions src/dfx/src/commands/deps/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use crate::lib::deps::{
};
use crate::lib::environment::Environment;
use crate::lib::error::DfxResult;
use crate::util::{check_candid_file, fuzzy_parse_argument};
use crate::util::fuzzy_parse_argument;
use anyhow::{anyhow, bail};
use candid::Principal;
use candid_parser::{types::IDLTypes, typing::ast_to_type};
use candid_parser::{types::IDLTypes, typing::ast_to_type, utils::CandidSource};
use clap::Parser;
use slog::{info, warn, Logger};

Expand Down Expand Up @@ -68,7 +68,7 @@ fn set_init(
.ok_or_else(|| anyhow!("Failed to find {canister_id} entry in pulled.json"))?;
let canister_prompt = get_canister_prompt(canister_id, pulled_canister);
let idl_path = get_pulled_service_candid_path(canister_id)?;
let (env, _) = check_candid_file(&idl_path)?;
let (env, _) = CandidSource::File(&idl_path).load()?;
let candid_args = pulled_json.get_candid_args(canister_id)?;
let candid_args_idl_types: IDLTypes = candid_args.parse()?;
let mut types = vec![];
Expand Down
4 changes: 2 additions & 2 deletions src/dfx/src/commands/remote/generate_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use crate::lib::agent::create_agent_environment;
use crate::lib::environment::Environment;
use crate::lib::error::DfxResult;
use crate::lib::models::canister::CanisterPool;
use crate::util::check_candid_file;
use anyhow::Context;
use candid_parser::utils::CandidSource;
use clap::Parser;
use slog::info;

Expand Down Expand Up @@ -70,7 +70,7 @@ pub fn exec(env: &dyn Environment, opts: GenerateBindingOpts) -> DfxResult {
continue;
}
}
let (type_env, did_types) = check_candid_file(&candid)?;
let (type_env, did_types) = CandidSource::File(&candid).load()?;
let extension = main.extension().unwrap_or_default();
let bindings = if extension == "mo" {
Some(candid_parser::bindings::motoko::compile(
Expand Down
4 changes: 2 additions & 2 deletions src/dfx/src/lib/builders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use crate::lib::canister_info::CanisterInfo;
use crate::lib::environment::Environment;
use crate::lib::error::{BuildError, DfxError, DfxResult};
use crate::lib::models::canister::CanisterPool;
use crate::util::check_candid_file;
use anyhow::{anyhow, bail, Context};
use candid::Principal as CanisterId;
use candid_parser::utils::CandidSource;
use dfx_core::config::model::dfinity::{Config, Profile};
use dfx_core::network::provider::get_network_context;
use dfx_core::util;
Expand Down Expand Up @@ -150,7 +150,7 @@ pub trait CanisterBuilder {

let generated_idl_path = self.generate_idl(pool, info, config)?;

let (env, ty) = check_candid_file(generated_idl_path.as_path())?;
let (env, ty) = CandidSource::File(generated_idl_path.as_path()).load()?;

// Typescript
if bindings.contains(&"ts".to_string()) {
Expand Down
36 changes: 20 additions & 16 deletions src/dfx/src/lib/models/canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ use crate::lib::error::{BuildError, DfxError, DfxResult};
use crate::lib::metadata::dfx::DfxMetadata;
use crate::lib::metadata::names::{CANDID_ARGS, CANDID_SERVICE, DFX};
use crate::lib::wasm::file::{compress_bytes, read_wasm_module};
use crate::util::{assets, check_candid_file};
use crate::util::assets;
use anyhow::{anyhow, bail, Context};
use candid::Principal as CanisterId;
use candid_parser::utils::CandidSource;
use dfx_core::config::model::canister_id_store::CanisterIdStore;
use dfx_core::config::model::dfinity::{
CanisterMetadataSection, Config, MetadataVisibility, WasmOptLevel,
Expand Down Expand Up @@ -275,15 +276,15 @@ impl Canister {

let IdlBuildOutput::File(build_idl_path) = &build_output.idl;

// 1. Copy the complete IDL file to .dfx/local/canisters/NAME/constructor.did.
// 1. Separate into constructor.did, service.did and init_args
let (constructor_did, service_did, init_args) = separate_candid(build_idl_path)?;

// 2. Copy the constructor IDL file to .dfx/local/canisters/NAME/constructor.did.
let constructor_idl_path = self.info.get_constructor_idl_path();
dfx_core::fs::composite::ensure_parent_dir_exists(&constructor_idl_path)?;
dfx_core::fs::copy(build_idl_path, &constructor_idl_path)?;
dfx_core::fs::write(build_idl_path, constructor_did)?;
dfx_core::fs::set_permissions_readwrite(&constructor_idl_path)?;

// 2. Separate into service.did and init_args
let (service_did, init_args) = separate_candid(build_idl_path)?;

// 3. Save service.did into following places in .dfx/local/:
// - canisters/NAME/service.did
// - IDL_ROOT/CANISTER_ID.did
Expand Down Expand Up @@ -337,7 +338,7 @@ fn wasm_opt_level_convert(opt_level: WasmOptLevel) -> OptLevel {
}
}

fn separate_candid(path: &Path) -> DfxResult<(String, String)> {
fn separate_candid(path: &Path) -> DfxResult<(String, String, String)> {
use candid::pretty::candid::{compile, pp_args};
use candid::types::internal::TypeInner;
use candid_parser::{
Expand All @@ -350,34 +351,37 @@ fn separate_candid(path: &Path) -> DfxResult<(String, String)> {
.decs
.iter()
.any(|dec| matches!(dec, Dec::ImportType(_) | Dec::ImportServ(_)));
let (env, actor) = check_candid_file(path)?;
let (env, actor) = CandidSource::File(path).load()?;
let actor = actor.ok_or_else(|| anyhow!("provided candid file contains no main service"))?;
let actor = env.trace_type(&actor)?;
let has_init_args = matches!(actor.as_ref(), TypeInner::Class(_, _));
if has_imports || has_init_args {
let (init_args, serv) = match actor.as_ref() {
TypeInner::Class(args, ty) => (args.clone(), ty.clone()),
TypeInner::Service(_) => (vec![], actor),
TypeInner::Service(_) => (vec![], actor.clone()),
_ => unreachable!(),
};
let init_args = pp_args(&init_args).pretty(80).to_string();
let service = compile(&env, &Some(serv));
Ok((service, init_args))
let constructor = compile(&env, &Some(actor));
Ok((constructor, service, init_args))
} else {
// Keep the original did file to preserve comments
Ok((did, "()".to_string()))
Ok((did.clone(), did, "()".to_string()))
}
}

#[context("{} is not a valid subtype of {}", specified_idl_path.display(), compiled_idl_path.display())]
fn check_valid_subtype(compiled_idl_path: &Path, specified_idl_path: &Path) -> DfxResult {
use candid::types::subtype::{subtype_with_config, OptReport};
let (mut env, opt_specified) =
check_candid_file(specified_idl_path).context("Checking specified candid file.")?;
let (mut env, opt_specified) = CandidSource::File(specified_idl_path)
.load()
.context("Checking specified candid file.")?;
let specified_type =
opt_specified.expect("Specified did file should contain some service interface");
let (env2, opt_compiled) =
check_candid_file(compiled_idl_path).context("Checking compiled candid file.")?;
let (env2, opt_compiled) = CandidSource::File(compiled_idl_path)
.load()
.context("Checking compiled candid file.")?;
let compiled_type =
opt_compiled.expect("Compiled did file should contain some service interface");
let mut gamma = HashSet::new();
Expand Down Expand Up @@ -828,7 +832,7 @@ fn build_canister_js(canister_id: &CanisterId, canister_info: &CanisterInfo) ->
.get_service_idl_path()
.with_extension("did.d.ts");

let (env, ty) = check_candid_file(&canister_info.get_service_idl_path())?;
let (env, ty) = CandidSource::File(&canister_info.get_constructor_idl_path()).load()?;
let content = ensure_trailing_newline(candid_parser::bindings::javascript::compile(&env, &ty));
std::fs::write(&output_did_js_path, content).with_context(|| {
format!(
Expand Down
11 changes: 7 additions & 4 deletions src/dfx/src/lib/operations/canister/install_canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@ fn check_candid_compatibility(
canister_info: &CanisterInfo,
candid: &str,
) -> anyhow::Result<Option<String>> {
use crate::util::check_candid_file;
use candid::types::subtype::{subtype_with_config, OptReport};
use candid_parser::utils::CandidSource;
let candid_path = canister_info.get_constructor_idl_path();
let deployed_path = canister_info
.get_constructor_idl_path()
Expand All @@ -261,11 +261,14 @@ fn check_candid_compatibility(
deployed_path.to_string_lossy()
)
})?;
let (mut env, opt_new) =
check_candid_file(&candid_path).context("Checking generated did file.")?;
let (mut env, opt_new) = CandidSource::File(&candid_path)
.load()
.context("Checking generated did file.")?;
let new_type = opt_new
.ok_or_else(|| anyhow!("Generated did file should contain some service interface"))?;
let (env2, opt_old) = check_candid_file(&deployed_path).context("Checking old candid file.")?;
let (env2, opt_old) = CandidSource::File(&deployed_path)
.load()
.context("Checking old candid file.")?;
let old_type = opt_old
.ok_or_else(|| anyhow!("Deployed did file should contain some service interface"))?;
let mut gamma = HashSet::new();
Expand Down
43 changes: 25 additions & 18 deletions src/dfx/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use backoff::backoff::Backoff;
use backoff::ExponentialBackoff;
use bytes::Bytes;
use candid::types::{value::IDLValue, Function, Type, TypeEnv, TypeInner};
use candid::IDLArgs;
use candid::{Decode, Encode, IDLArgs, Principal};
use candid_parser::error::pretty_diagnose;
use candid_parser::typing::pretty_check_file;
use candid_parser::utils::CandidSource;
use dfx_core::fs::create_dir_all;
use fn_error_context::context;
#[cfg(unix)]
Expand Down Expand Up @@ -109,21 +109,38 @@ pub async fn read_module_metadata(
)
}

pub async fn fetch_remote_did_file(
agent: &ic_agent::Agent,
canister_id: Principal,
) -> Option<String> {
Some(
match read_module_metadata(agent, canister_id, "candid:service").await {
Some(candid) => candid,
None => {
let bytes = agent
.query(&canister_id, "__get_candid_interface_tmp_hack")
.with_arg(Encode!().ok()?)
.call()
.await
.ok()?;
Decode!(&bytes, String).ok()?
}
},
)
}

/// Parse IDL file into TypeEnv. This is a best effort function: it will succeed if
/// the IDL file can be parsed and type checked in Rust parser, and has an
/// actor in the IDL file. If anything fails, it returns None.
pub fn get_candid_type(
idl_path: &std::path::Path,
method_name: &str,
) -> Option<(TypeEnv, Function)> {
let (env, ty) = check_candid_file(idl_path).ok()?;
pub fn get_candid_type(candid: CandidSource, method_name: &str) -> Option<(TypeEnv, Function)> {
let (env, ty) = candid.load().ok()?;
let actor = ty?;
let method = env.get_method(&actor, method_name).ok()?.clone();
Some((env, method))
}

pub fn get_candid_init_type(idl_path: &std::path::Path) -> Option<(TypeEnv, Function)> {
let (env, ty) = check_candid_file(idl_path).ok()?;
let (env, ty) = CandidSource::File(idl_path).load().ok()?;
let actor = ty?;
let args = match actor.as_ref() {
TypeInner::Class(args, _) => args.clone(),
Expand All @@ -137,16 +154,6 @@ pub fn get_candid_init_type(idl_path: &std::path::Path) -> Option<(TypeEnv, Func
Some((env, res))
}

pub fn check_candid_file(idl_path: &std::path::Path) -> DfxResult<(TypeEnv, Option<Type>)> {
//context macro does not work for the returned error type
pretty_check_file(idl_path).with_context(|| {
format!(
"Candid file check failed for {}.",
idl_path.to_string_lossy()
)
})
}

pub fn arguments_from_file(file_name: &Path) -> DfxResult<String> {
if file_name == Path::new("-") {
let mut content = String::new();
Expand Down

0 comments on commit 28dc1e4

Please sign in to comment.