diff --git a/dfx/Cargo.lock b/dfx/Cargo.lock index 2a51b03ecf..ee54330390 100644 --- a/dfx/Cargo.lock +++ b/dfx/Cargo.lock @@ -314,6 +314,7 @@ dependencies = [ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "indicatif 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "mockito 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", "serde_bytes 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -758,7 +759,7 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -979,13 +980,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "getrandom 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1004,7 +1005,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1022,7 +1023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "rand_core" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "getrandom 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1041,7 +1042,7 @@ name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1380,7 +1381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1919,12 +1920,12 @@ dependencies = [ "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" "checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" "checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -"checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c" +"checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" "checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" "checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" -"checksum rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "615e683324e75af5d43d8f7a39ffe3ee4a9dc42c5c701167a71dc59c3a493aca" +"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" "checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" "checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" diff --git a/dfx/Cargo.toml b/dfx/Cargo.toml index b2262b5d4c..4ba99e0cb0 100644 --- a/dfx/Cargo.toml +++ b/dfx/Cargo.toml @@ -19,6 +19,7 @@ console = "0.7.7" flate2 = "1.0.11" futures = "0.1.28" indicatif = "0.12.0" +rand = "0.7.2" reqwest = "0.9.20" serde = "1.0" serde_bytes = "0.11.2" diff --git a/dfx/src/commands/call.rs b/dfx/src/commands/call.rs deleted file mode 100644 index 065a19c1fa..0000000000 --- a/dfx/src/commands/call.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::lib::api_client::*; -use crate::lib::env::ClientEnv; -use crate::lib::error::{DfxError, DfxResult}; -use clap::{App, Arg, ArgMatches, SubCommand}; -use futures::future::{err, ok, Future}; -use tokio::runtime::Runtime; - -const HOST_ARG: &str = "host"; -const NAME_ARG: &str = "name"; - -pub fn construct() -> App<'static, 'static> { - SubCommand::with_name("call") - .about(r#"Call a "greet" function on the canister with ID 42."#) - .arg( - Arg::with_name(HOST_ARG) - .help("The host (with port) to send the request to.") - .required(true), - ) - .arg( - Arg::with_name(NAME_ARG) - .help("The person to say hello to.") - .required(true), - ) -} - -pub fn exec(env: &T, args: &ArgMatches<'_>) -> DfxResult -where - T: ClientEnv, -{ - let name = args.value_of(NAME_ARG).unwrap(); - let client = env.get_client(); - - let query = query( - client, - CanisterQueryCall { - canister_id: 42, - method_name: "greet".to_string(), - arg: Blob(Vec::from(name)), - }, - ) - .and_then(|r| match r { - ReadResponse::Pending => { - println!("Pending"); - ok(()) - } - ReadResponse::Replied { - reply: QueryResponseReply { arg: Blob(blob) }, - } => { - println!("{}", String::from_utf8_lossy(&blob)); - ok(()) - } - ReadResponse::Rejected { - reject_code, - reject_message, - } => err(DfxError::ClientError(reject_code, reject_message)), - ReadResponse::Unknown => err(DfxError::Unknown("Unknown response".to_owned())), - }); - - let mut runtime = Runtime::new().expect("Unable to create a runtime"); - runtime.block_on(query) -} diff --git a/dfx/src/commands/canister/call.rs b/dfx/src/commands/canister/call.rs new file mode 100644 index 0000000000..e4ed45bc45 --- /dev/null +++ b/dfx/src/commands/canister/call.rs @@ -0,0 +1,53 @@ +use crate::lib::api_client::{call, Blob}; +use crate::lib::env::ClientEnv; +use crate::lib::error::DfxResult; +use crate::lib::CanisterId; +use crate::util::clap::validators; +use clap::{App, Arg, ArgMatches, SubCommand}; +use tokio::runtime::Runtime; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("call") + .about("Call a canister.") + .arg( + Arg::with_name("canister") + .takes_value(true) + .help("The canister ID (a number) to call.") + .required(true) + .validator(validators::is_canister_id), + ) + .arg( + Arg::with_name("method_name") + .help("The method name file to use.") + .required(true), + ) + .arg( + Arg::with_name("arguments") + .help("Arguments to pass to the method.") + .takes_value(true) + .multiple(true), + ) +} + +pub fn exec(env: &T, args: &ArgMatches<'_>) -> DfxResult +where + T: ClientEnv, +{ + // Read the config. + let canister_id = args.value_of("canister").unwrap().parse::()?; + let method_name = args.value_of("method_name").unwrap(); + let arguments: Option> = args.values_of("arguments").map(|args| args.collect()); + + let client = env.get_client(); + let install = call( + client, + canister_id, + method_name.to_owned(), + arguments.map(|args| Blob(Vec::from(args[0]))), + ); + + let mut runtime = Runtime::new().expect("Unable to create a runtime"); + runtime.block_on(install)?; + + Ok(()) +} diff --git a/dfx/src/commands/canister/install.rs b/dfx/src/commands/canister/install.rs index 0b74554a28..e476e40f1d 100644 --- a/dfx/src/commands/canister/install.rs +++ b/dfx/src/commands/canister/install.rs @@ -1,16 +1,12 @@ use crate::lib::api_client::{install_code, Blob}; use crate::lib::env::{ClientEnv, ProjectConfigEnv}; use crate::lib::error::DfxResult; +use crate::lib::CanisterId; +use crate::util::clap::validators; use clap::{App, Arg, ArgMatches, SubCommand}; use std::path::PathBuf; use tokio::runtime::Runtime; -fn is_number(v: String) -> Result<(), String> { - v.parse::() - .map_err(|_| String::from("The value must be a number.")) - .map(|_| ()) -} - pub fn construct() -> App<'static, 'static> { SubCommand::with_name("install") .about("Install a canister.") @@ -19,7 +15,7 @@ pub fn construct() -> App<'static, 'static> { .takes_value(true) .help("The canister ID (a number).") .required(true) - .validator(is_number), + .validator(validators::is_canister_id), ) .arg( Arg::with_name("wasm") @@ -36,7 +32,7 @@ where let config = env.get_config().unwrap(); let project_root = config.get_path().parent().unwrap(); - let canister_id = args.value_of("canister").unwrap().parse::()?; + let canister_id = args.value_of("canister").unwrap().parse::()?; let wasm_path = args.value_of("wasm").unwrap(); let wasm_path = PathBuf::from(project_root).join(wasm_path); diff --git a/dfx/src/commands/canister/mod.rs b/dfx/src/commands/canister/mod.rs index cbe2915583..5a1a3ec0f4 100644 --- a/dfx/src/commands/canister/mod.rs +++ b/dfx/src/commands/canister/mod.rs @@ -3,13 +3,19 @@ use crate::lib::env::{ClientEnv, ProjectConfigEnv}; use crate::lib::error::{DfxError, DfxResult}; use clap::{App, ArgMatches, SubCommand}; +mod call; mod install; +mod query; fn builtins() -> Vec> where T: ClientEnv + ProjectConfigEnv, { - vec![CliCommand::new(install::construct(), install::exec)] + vec![ + CliCommand::new(call::construct(), call::exec), + CliCommand::new(install::construct(), install::exec), + CliCommand::new(query::construct(), query::exec), + ] } pub fn construct() -> App<'static, 'static> diff --git a/dfx/src/commands/canister/query.rs b/dfx/src/commands/canister/query.rs new file mode 100644 index 0000000000..cf23763db0 --- /dev/null +++ b/dfx/src/commands/canister/query.rs @@ -0,0 +1,69 @@ +use crate::lib::api_client::{query, Blob, QueryResponseReply, ReadResponse}; +use crate::lib::env::ClientEnv; +use crate::lib::error::{DfxError, DfxResult}; +use crate::lib::CanisterId; +use crate::util::clap::validators; +use clap::{App, Arg, ArgMatches, SubCommand}; +use tokio::runtime::Runtime; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("query") + .about("Query a canister.") + .arg( + Arg::with_name("canister") + .takes_value(true) + .help("The canister ID (a number) to query.") + .required(true) + .validator(validators::is_canister_id), + ) + .arg( + Arg::with_name("method_name") + .help("The name of the method to query.") + .required(true), + ) + .arg( + Arg::with_name("arguments") + .help("Arguments to pass to the method.") + .takes_value(true) + .multiple(true), + ) +} + +pub fn exec(env: &T, args: &ArgMatches<'_>) -> DfxResult +where + T: ClientEnv, +{ + // Read the config. + let canister_id = args.value_of("canister").unwrap().parse::()?; + let method_name = args.value_of("method_name").unwrap(); + let arguments: Option> = args.values_of("arguments").map(|args| args.collect()); + + let client = env.get_client(); + let install = query( + client, + canister_id, + method_name.to_owned(), + arguments.map(|args| Blob(Vec::from(args[0]))), + ); + + let mut runtime = Runtime::new().expect("Unable to create a runtime"); + match runtime.block_on(install) { + Ok(ReadResponse::Pending) => { + println!("Pending"); + Ok(()) + } + Ok(ReadResponse::Replied { + reply: QueryResponseReply { arg: Blob(blob) }, + }) => { + println!("{}", String::from_utf8_lossy(&blob)); + Ok(()) + } + Ok(ReadResponse::Rejected { + reject_code, + reject_message, + }) => Err(DfxError::ClientError(reject_code, reject_message)), + // TODO: remove this when moving to ic_http_api. + Ok(ReadResponse::Unknown) => Err(DfxError::Unknown("Unknown response".to_owned())), + Err(x) => Err(x), + } +} diff --git a/dfx/src/commands/mod.rs b/dfx/src/commands/mod.rs index 6059f23e9b..69a9d0a1e6 100644 --- a/dfx/src/commands/mod.rs +++ b/dfx/src/commands/mod.rs @@ -5,7 +5,6 @@ use crate::lib::error::DfxResult; use clap::ArgMatches; mod build; -mod call; mod canister; mod new; mod start; @@ -41,7 +40,6 @@ where { vec![ CliCommand::new(build::construct(), build::exec), - CliCommand::new(call::construct(), call::exec), CliCommand::new(canister::construct::(), canister::exec), CliCommand::new(new::construct(), new::exec), CliCommand::new(start::construct(), start::exec), diff --git a/dfx/src/lib/api_client.rs b/dfx/src/lib/api_client.rs index d2a3c5f886..61fec4a42a 100644 --- a/dfx/src/lib/api_client.rs +++ b/dfx/src/lib/api_client.rs @@ -1,6 +1,8 @@ use crate::lib::error::*; +use crate::lib::CanisterId; use futures::future::{err, ok, result, Future}; use futures::stream::Stream; +use rand::Rng; use reqwest::r#async::Client as ReqwestClient; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -12,8 +14,6 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; // bytestring pub struct Blob(#[serde(with = "serde_bytes")] pub Vec); -type CanisterId = u64; - #[derive(Clone)] pub struct Client { client: ReqwestClient, @@ -90,11 +90,27 @@ enum SubmitRequest { canister_id: CanisterId, module: Blob, arg: Blob, + nonce: Option, }, + Call { + canister_id: CanisterId, + method_name: String, + arg: Blob, + nonce: Option, + }, +} + +/// Generates a random 32 bytes of blob. +fn random_blob() -> Blob { + let mut rng = rand::thread_rng(); + Blob(rng.gen::<[u8; 32]>().iter().cloned().collect()) } /// A read request. Intended to remain private in favor of exposing specialized /// functions like `query` instead. +/// +/// TODO: filter the output of this function when moving to ic_http_api. +/// For example, it should never return Unknown or Pending, per the spec. fn read( client: Client, request: ReadRequest, @@ -163,9 +179,20 @@ fn submit( /// returns the canister’s response directly within the HTTP response. pub fn query( client: Client, - request: CanisterQueryCall, + canister_id: CanisterId, + method_name: String, + arg: Option, ) -> impl Future, Error = DfxError> { - read(client, ReadRequest::Query { request }) + read( + client, + ReadRequest::Query { + request: CanisterQueryCall { + canister_id, + method_name, + arg: arg.unwrap_or_else(|| Blob(vec![])), + }, + }, + ) } /// Canister Install call @@ -181,6 +208,36 @@ pub fn install_code( canister_id, module, arg: arg.unwrap_or_else(|| Blob(vec![])), + nonce: Some(random_blob()), + }, + ) + .and_then(|response| { + result( + response + .error_for_status() + .map(|_| ()) + .map_err(DfxError::from), + ) + }) +} + +/// Canister call +/// +/// Canister methods that can change the canister state. This return right away, and cannot wait +/// for the canister to be done. +pub fn call( + client: Client, + canister_id: CanisterId, + method_name: String, + arg: Option, +) -> impl Future { + submit( + client, + SubmitRequest::Call { + canister_id, + method_name, + arg: arg.unwrap_or_else(|| Blob(vec![])), + nonce: Some(random_blob()), }, ) .and_then(|response| { @@ -312,14 +369,7 @@ mod tests { url: mockito::server_url(), }); - let query = query( - client, - CanisterQueryCall { - canister_id: 1, - method_name: "main".to_string(), - arg: Blob(vec![]), - }, - ); + let query = query(client, 1, "main".to_string(), Some(Blob(vec![]))); let mut runtime = tokio::runtime::Runtime::new().expect("Unable to create a runtime"); let result = runtime.block_on(query); @@ -384,14 +434,7 @@ mod tests { url: mockito::server_url(), }); - let query = query( - client, - CanisterQueryCall { - canister_id: 1, - method_name: "main".to_string(), - arg: Blob(vec![]), - }, - ); + let query = query(client, 1, "main".to_string(), Some(Blob(vec![]))); let mut runtime = tokio::runtime::Runtime::new().expect("Unable to create a runtime"); let result = runtime.block_on(query); @@ -417,6 +460,7 @@ mod tests { canister_id, module, arg, + nonce: None, }; let actual: Value = serde_cbor::from_slice(&serde_cbor::to_vec(&request).unwrap()).unwrap(); @@ -433,6 +477,7 @@ mod tests { ), (Value::Text("module".to_string()), Value::Bytes(vec![1])), (Value::Text("arg".to_string()), Value::Bytes(vec![2])), + (Value::Text("nonce".to_string()), Value::Null), ] .into_iter() .collect(), diff --git a/dfx/src/lib/mod.rs b/dfx/src/lib/mod.rs index bb294ecd56..55b7887474 100644 --- a/dfx/src/lib/mod.rs +++ b/dfx/src/lib/mod.rs @@ -1,3 +1,5 @@ pub mod api_client; pub mod env; pub mod error; + +pub type CanisterId = u64; diff --git a/dfx/src/util/clap/mod.rs b/dfx/src/util/clap/mod.rs new file mode 100644 index 0000000000..f3826ce00e --- /dev/null +++ b/dfx/src/util/clap/mod.rs @@ -0,0 +1 @@ +pub mod validators; diff --git a/dfx/src/util/clap/validators.rs b/dfx/src/util/clap/validators.rs new file mode 100644 index 0000000000..d1aef12828 --- /dev/null +++ b/dfx/src/util/clap/validators.rs @@ -0,0 +1,7 @@ +use crate::lib::CanisterId; + +pub fn is_canister_id(v: String) -> Result<(), String> { + v.parse::() + .map_err(|_| String::from("The value must be a canister ID (number).")) + .map(|_| ()) +} diff --git a/dfx/src/util/mod.rs b/dfx/src/util/mod.rs index 5d8c80bf4e..e81b83abe2 100644 --- a/dfx/src/util/mod.rs +++ b/dfx/src/util/mod.rs @@ -1 +1,2 @@ pub mod assets; +pub mod clap;