diff --git a/Cargo.lock b/Cargo.lock index 13764bdd1c..e72900162f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -706,6 +706,8 @@ dependencies = [ "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "flate2 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ic-http-agent 0.1.0", "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)", @@ -789,6 +791,15 @@ dependencies = [ "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "error" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "error-chain" version = "0.12.1" @@ -954,6 +965,11 @@ name = "hex" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "hex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "hostname" version = "0.1.5" @@ -1038,6 +1054,21 @@ dependencies = [ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ic-http-agent" +version = "0.1.0" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.10.24 (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.101 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_bytes 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_cbor 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "idna" version = "0.1.5" @@ -2309,6 +2340,11 @@ dependencies = [ "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "treeline" version = "0.1.0" @@ -2369,6 +2405,11 @@ dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "typeable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicase" version = "2.5.1" @@ -2678,6 +2719,7 @@ dependencies = [ "checksum encoding_rs 0.8.20 (registry+https://github.com/rust-lang/crates.io-index)" = "87240518927716f79692c2ed85bfe6e98196d18c6401ec75355760233a7e12e9" "checksum enum-as-inner 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3d58266c97445680766be408285e798d3401c6d4c378ec5552e78737e681e37d" "checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" +"checksum error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e606f14042bb87cc02ef6a14db6c90ab92ed6f62d87e69377bc759fd7987cc" "checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9" "checksum escargot 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ceb9adbf9874d5d028b5e4c5739d22b71988252b25c9c98fe7cf9738bee84597" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" @@ -2699,6 +2741,7 @@ dependencies = [ "checksum hashbrown 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "29fba9abe4742d586dfd0c06ae4f7e73a1c2d86b856933509b269d82cdf06e18" "checksum hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e1de41fb8dba9714efd92241565cdff73f78508c95697dd56787d3cba27e2353" "checksum hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" +"checksum hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e" "checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" "checksum http 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "372bcb56f939e449117fb0869c2e8fd8753a8223d92a172c6e808cf123a5b6e4" "checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" @@ -2843,11 +2886,13 @@ dependencies = [ "checksum tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "f2106812d500ed25a4f38235b9cae8f78a09edf43203e16e59c3b769a342a60e" "checksum tokio-udp 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f02298505547f73e60f568359ef0d016d5acd6e830ab9bc7c4a5b3403440121b" "checksum tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "037ffc3ba0e12a0ab4aca92e5234e0dedeb48fddf6ccd260f1f150a36a9f2445" +"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" "checksum treeline 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" "checksum trust-dns-proto 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5559ebdf6c2368ddd11e20b11d6bbaf9e46deb803acd7815e93f5a7b4a6d2901" "checksum trust-dns-resolver 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c9992e58dba365798803c0b91018ff6c8d3fc77e06977c4539af2a6bfe0a039" "checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" "checksum try_from 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" +"checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" "checksum unicase 2.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2e2e6bd1e59e56598518beb94fd6db628ded570326f0a98c679a304bd9f00150" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426" diff --git a/Cargo.toml b/Cargo.toml index 7a127a2bc1..b4a3132103 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,8 @@ -[root] -name = "dfinity-sdk" -version = "0.2.1" - [workspace] members = [ "dfx", "lib/dfx_derive", "lib/dfx_info", + "lib/ic_http_agent", "lib/serde_idl" ] diff --git a/dfx/Cargo.toml b/dfx/Cargo.toml index 22f028daab..fea972a0cf 100644 --- a/dfx/Cargo.toml +++ b/dfx/Cargo.toml @@ -21,6 +21,8 @@ clap = "2.33.0" console = "0.7.7" flate2 = "1.0.11" futures = "0.1.28" +hex = "0.3.2" +ic-http-agent = { "path" = "../lib/ic_http_agent" } indicatif = "0.12.0" rand = "0.7.2" reqwest = "0.9.20" diff --git a/dfx/src/commands/canister/call.rs b/dfx/src/commands/canister/call.rs index e4ed45bc45..99c6ea6a0f 100644 --- a/dfx/src/commands/canister/call.rs +++ b/dfx/src/commands/canister/call.rs @@ -1,9 +1,9 @@ -use crate::lib::api_client::{call, Blob}; +use crate::lib::api_client::{call, request_status, QueryResponseReply, ReadResponse}; use crate::lib::env::ClientEnv; -use crate::lib::error::DfxResult; -use crate::lib::CanisterId; +use crate::lib::error::{DfxError, DfxResult}; use crate::util::clap::validators; use clap::{App, Arg, ArgMatches, SubCommand}; +use ic_http_agent::{Blob, CanisterId}; use tokio::runtime::Runtime; pub fn construct() -> App<'static, 'static> { @@ -21,6 +21,13 @@ pub fn construct() -> App<'static, 'static> { .help("The method name file to use.") .required(true), ) + .arg( + Arg::with_name("wait") + .help("Wait for the result of the call, by polling the client.") + .long("wait") + .short("w") + .takes_value(false), + ) .arg( Arg::with_name("arguments") .help("Arguments to pass to the method.") @@ -47,7 +54,32 @@ where ); let mut runtime = Runtime::new().expect("Unable to create a runtime"); - runtime.block_on(install)?; + let request_id = runtime.block_on(install)?; - Ok(()) + if args.is_present("wait") { + let request_status = request_status(env.get_client(), request_id); + let mut runtime = Runtime::new().expect("Unable to create a runtime"); + match runtime.block_on(request_status) { + Ok(ReadResponse::Pending) => { + println!("Pending"); + Ok(()) + } + Ok(ReadResponse::Replied { reply }) => { + if let Some(QueryResponseReply { arg: Blob(blob) }) = reply { + println!("{}", String::from_utf8_lossy(&blob)); + } + Ok(()) + } + Ok(ReadResponse::Rejected { + reject_code, + reject_message, + }) => Err(DfxError::ClientError(reject_code, reject_message)), + // TODO(SDK-446): remove this when moving api_client to ic_http_agent. + Ok(ReadResponse::Unknown) => Err(DfxError::Unknown("Unknown response".to_owned())), + Err(x) => Err(x), + } + } else { + println!("0x{}", String::from(request_id)); + Ok(()) + } } diff --git a/dfx/src/commands/canister/install.rs b/dfx/src/commands/canister/install.rs index e476e40f1d..02edb37984 100644 --- a/dfx/src/commands/canister/install.rs +++ b/dfx/src/commands/canister/install.rs @@ -1,9 +1,9 @@ -use crate::lib::api_client::{install_code, Blob}; +use crate::lib::api_client::install_code; 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 ic_http_agent::{Blob, CanisterId}; use std::path::PathBuf; use tokio::runtime::Runtime; diff --git a/dfx/src/commands/canister/mod.rs b/dfx/src/commands/canister/mod.rs index 5a1a3ec0f4..8dad415ccd 100644 --- a/dfx/src/commands/canister/mod.rs +++ b/dfx/src/commands/canister/mod.rs @@ -6,6 +6,7 @@ use clap::{App, ArgMatches, SubCommand}; mod call; mod install; mod query; +mod request_status; fn builtins() -> Vec> where @@ -15,6 +16,7 @@ where CliCommand::new(call::construct(), call::exec), CliCommand::new(install::construct(), install::exec), CliCommand::new(query::construct(), query::exec), + CliCommand::new(request_status::construct(), request_status::exec), ] } diff --git a/dfx/src/commands/canister/query.rs b/dfx/src/commands/canister/query.rs index cf23763db0..81a1cbb1e3 100644 --- a/dfx/src/commands/canister/query.rs +++ b/dfx/src/commands/canister/query.rs @@ -1,9 +1,9 @@ -use crate::lib::api_client::{query, Blob, QueryResponseReply, ReadResponse}; +use crate::lib::api_client::{query, 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 ic_http_agent::{Blob, CanisterId}; use tokio::runtime::Runtime; pub fn construct() -> App<'static, 'static> { @@ -52,17 +52,17 @@ where println!("Pending"); Ok(()) } - Ok(ReadResponse::Replied { - reply: QueryResponseReply { arg: Blob(blob) }, - }) => { - println!("{}", String::from_utf8_lossy(&blob)); + Ok(ReadResponse::Replied { reply }) => { + if let Some(QueryResponseReply { arg: Blob(blob) }) = reply { + 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. + // TODO(SDK-446): remove this when moving api_client to ic_http_agent. Ok(ReadResponse::Unknown) => Err(DfxError::Unknown("Unknown response".to_owned())), Err(x) => Err(x), } diff --git a/dfx/src/commands/canister/request_status.rs b/dfx/src/commands/canister/request_status.rs new file mode 100644 index 0000000000..82f42db2f1 --- /dev/null +++ b/dfx/src/commands/canister/request_status.rs @@ -0,0 +1,48 @@ +use crate::lib::api_client::{request_status, QueryResponseReply, ReadResponse}; +use crate::lib::env::ClientEnv; +use crate::lib::error::{DfxError, DfxResult}; +use crate::util::clap::validators; +use clap::{App, Arg, ArgMatches, SubCommand}; +use ic_http_agent::{Blob, RequestId}; +use std::str::FromStr; +use tokio::runtime::Runtime; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("request-status") + .about("Request the status of a call to a canister.") + .arg( + Arg::with_name("request_id") + .takes_value(true) + .help("The request ID to call. This is an hexadecimal string starting with 0x.") + .required(true) + .validator(validators::is_request_id), + ) +} + +pub fn exec(env: &T, args: &ArgMatches<'_>) -> DfxResult +where + T: ClientEnv, +{ + let request_id = RequestId::from_str(&args.value_of("request_id").unwrap()[2..])?; + let request_status = request_status(env.get_client(), request_id); + let mut runtime = Runtime::new().expect("Unable to create a runtime"); + match runtime.block_on(request_status) { + Ok(ReadResponse::Pending) => { + println!("Pending"); + Ok(()) + } + Ok(ReadResponse::Replied { reply }) => { + if let Some(QueryResponseReply { arg: Blob(blob) }) = reply { + println!("{}", String::from_utf8_lossy(&blob)); + } + Ok(()) + } + Ok(ReadResponse::Rejected { + reject_code, + reject_message, + }) => Err(DfxError::ClientError(reject_code, reject_message)), + // TODO(SDK-446): remove this when moving api_client to ic_http_agent. + Ok(ReadResponse::Unknown) => Err(DfxError::Unknown("Unknown response".to_owned())), + Err(x) => Err(x), + } +} diff --git a/dfx/src/commands/new.rs b/dfx/src/commands/new.rs index 65a42c2667..32cc7e39bb 100644 --- a/dfx/src/commands/new.rs +++ b/dfx/src/commands/new.rs @@ -97,7 +97,6 @@ where b.set_message("Looking for latest version..."); b.enable_steady_tick(80); - std::thread::sleep(std::time::Duration::from_secs(1)); if !env.is_installed()? { env.install()?; b.finish_with_message( diff --git a/dfx/src/commands/start.rs b/dfx/src/commands/start.rs index 6a8cfa1a2f..9c66aea18c 100644 --- a/dfx/src/commands/start.rs +++ b/dfx/src/commands/start.rs @@ -4,6 +4,7 @@ use crate::lib::error::{DfxError, DfxResult}; use crate::lib::webserver::webserver; use clap::{App, Arg, ArgMatches, SubCommand}; use indicatif::{ProgressBar, ProgressDrawTarget}; +use std::process::Command; use std::time::{Duration, Instant}; use tokio::prelude::FutureExt; use tokio::runtime::Runtime; @@ -20,20 +21,47 @@ pub fn construct() -> App<'static, 'static> { .long("host") .takes_value(true), ) + .arg( + Arg::with_name("background") + .help("Exit the dfx leaving the client running. Will wait until the client replies before exiting.") + .long("background") + .takes_value(false), + ) +} + +fn ping_and_wait(frontend_url: &str) -> DfxResult { + std::thread::sleep(Duration::from_millis(500)); + + let mut runtime = Runtime::new().expect("Unable to create a runtime"); + + // Try to ping for 1 second, then timeout after 5 seconds if ping hasn't succeeded. + let start = Instant::now(); + while { + let client = Client::new(ClientConfig { + url: frontend_url.to_string(), + }); + + runtime + .block_on(ping(client).timeout(Duration::from_millis(300))) + .is_err() + } { + if Instant::now().duration_since(start) > Duration::from_secs(TIMEOUT_IN_SECS) { + return Err(DfxError::Unknown( + "Timeout during start of the client.".to_owned(), + )); + } + std::thread::sleep(Duration::from_millis(200)); + } + + Ok(()) } pub fn exec(env: &T, args: &ArgMatches<'_>) -> DfxResult where T: ProjectConfigEnv + BinaryResolverEnv, { - let b = ProgressBar::new_spinner(); - b.set_draw_target(ProgressDrawTarget::stderr()); - - b.set_message("Starting up the client..."); - b.enable_steady_tick(80); - - let client_pathbuf = env.get_binary_command_path("client").unwrap(); - let nodemanager_pathbuf = env.get_binary_command_path("nodemanager").unwrap(); + let client_pathbuf = env.get_binary_command_path("client")?; + let nodemanager_pathbuf = env.get_binary_command_path("nodemanager")?; let config = env.get_config().unwrap(); let address_and_port = args @@ -54,23 +82,44 @@ where ); let project_root = config.get_path().parent().unwrap(); - let client_watchdog = std::thread::spawn(move || { - let client = client_pathbuf.as_path(); - let nodemanager = nodemanager_pathbuf.as_path(); - loop { - let mut cmd = std::process::Command::new(nodemanager); - cmd.args(&[client]); - cmd.stdout(std::process::Stdio::inherit()); - cmd.stderr(std::process::Stdio::inherit()); - - // If the nodemanager itself fails, we are probably deeper into troubles than - // we can solve at this point and the user is better rerunning the server. - let mut child = cmd.spawn().unwrap(); - if child.wait().is_err() { - break; + if args.is_present("background") { + // Background strategy is different; we spawn `dfx` with the same arguments + // (minus --background), ping and exit. + let exe = std::env::current_exe()?; + let mut cmd = Command::new(exe); + // Skip 1 because arg0 is this executable's path. + cmd.args(std::env::args().skip(1).filter(|a| !a.eq("--background"))); + + cmd.spawn()?; + + return ping_and_wait(&frontend_url); + } + + let b = ProgressBar::new_spinner(); + b.set_draw_target(ProgressDrawTarget::stderr()); + + b.set_message("Starting up the client..."); + b.enable_steady_tick(80); + + let client_watchdog = std::thread::Builder::new() + .name("NodeManager".into()) + .spawn(move || { + let client = client_pathbuf.as_path(); + let nodemanager = nodemanager_pathbuf.as_path(); + loop { + let mut cmd = std::process::Command::new(nodemanager); + cmd.args(&[client]); + cmd.stdout(std::process::Stdio::inherit()); + cmd.stderr(std::process::Stdio::inherit()); + + // If the nodemanager itself fails, we are probably deeper into troubles than + // we can solve at this point and the user is better rerunning the server. + let mut child = cmd.spawn().unwrap(); + if child.wait().is_err() { + break; + } } - } - }); + })?; let frontend_watchdog = webserver( address_and_port, url::Url::parse(IC_CLIENT_BIND_ADDR).unwrap(), @@ -87,30 +136,7 @@ where ); b.set_message("Pinging the DFINITY client..."); - - std::thread::sleep(Duration::from_millis(500)); - - let mut runtime = Runtime::new().expect("Unable to create a runtime"); - - // Try to ping for 1 second, then timeout after 5 seconds if ping hasn't succeeded. - let start = Instant::now(); - while { - let client = Client::new(ClientConfig { - url: frontend_url.clone(), - }); - - runtime - .block_on(ping(client).timeout(Duration::from_millis(TIMEOUT_IN_SECS * 1000 / 4))) - .is_err() - } { - if Instant::now().duration_since(start) > Duration::from_secs(TIMEOUT_IN_SECS) { - return Err(DfxError::Unknown( - "Timeout during start of the client.".to_owned(), - )); - } - std::thread::sleep(Duration::from_millis(100)); - } - + ping_and_wait(&frontend_url)?; b.finish_with_message("DFINITY client started..."); frontend_watchdog.join().unwrap(); diff --git a/dfx/src/config/cache.rs b/dfx/src/config/cache.rs index 3b0aae54cc..1de4ce6fb2 100644 --- a/dfx/src/config/cache.rs +++ b/dfx/src/config/cache.rs @@ -1,8 +1,8 @@ -use std::io::{Error, ErrorKind, Result}; -use std::path::PathBuf; - use crate::config::dfx_version; use crate::util; +use std::io::{Error, ErrorKind, Result}; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; pub fn get_bin_cache_root() -> Result { let home = match std::env::var("HOME") { @@ -55,7 +55,21 @@ pub fn install_version(v: &str) -> Result { } if v == dfx_version() { - util::assets::binary_cache()?.unpack(p.as_path())?; + let mut binary_cache_assets = util::assets::binary_cache()?; + // Write binaries and set them to be executable. + for file in binary_cache_assets.entries()? { + let mut file = file?; + + if file.header().entry_type().is_dir() { + continue; + } + file.unpack_in(p.as_path())?; + + let full_path = p.join(file.path()?); + let mut perms = std::fs::metadata(full_path.as_path())?.permissions(); + perms.set_mode(0o554); + std::fs::set_permissions(full_path.as_path(), perms)?; + } Ok(p) } else { Err(Error::new( diff --git a/dfx/src/lib/api_client.rs b/dfx/src/lib/api_client.rs index 32a8c95d85..9173474995 100644 --- a/dfx/src/lib/api_client.rs +++ b/dfx/src/lib/api_client.rs @@ -1,19 +1,13 @@ +/// TODO(SDK-446): Move everything Public Spec related from this file to the ic_http_agent library. use crate::lib::error::*; -use crate::lib::CanisterId; use futures::future::{err, ok, result, Future}; use futures::stream::Stream; +use ic_http_agent::{to_request_id, Blob, CanisterId, RequestId}; use rand::Rng; use reqwest::r#async::Client as ReqwestClient; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -/// A binary "blob", i.e. a byte array -#[derive(PartialEq, Eq, Serialize, Deserialize, Debug)] -// XXX: We newtype and make sure that serde uses `serde_bytes`, otherwise the `Vec` is -// deserialized as a sequence (array) of bytes, whereas we want an actual CBOR "byte array", e.g. a -// bytestring -pub struct Blob(#[serde(with = "serde_bytes")] pub Vec); - #[derive(Clone)] pub struct Client { client: ReqwestClient, @@ -44,24 +38,29 @@ pub struct ClientConfig { } /// Request payloads for the /api/v1/read endpoint. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] +/// This never needs to be deserialized. +#[derive(Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "kebab-case")] #[serde(tag = "request_type")] enum ReadRequest { Query { #[serde(flatten)] request: CanisterQueryCall, }, + RequestStatus { + request_id: RequestId, + }, } /// Response payloads for the /api/v1/read endpoint. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +/// This needs to be serialized for tests. +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] #[serde(tag = "status")] pub enum ReadResponse { Pending, Replied { - reply: A, + reply: Option, }, Rejected { reject_code: ReadRejectCode, @@ -109,7 +108,7 @@ fn random_blob() -> Blob { /// 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. +/// TODO: filter the output of this function when moving to ic_http_agent. /// For example, it should never return Unknown or Pending, per the spec. fn read( client: Client, @@ -135,7 +134,7 @@ where .and_then(|res| res.into_body().concat2().map_err(DfxError::Reqwest)) .and_then(|buf| match serde_cbor::from_slice(&buf) { Ok(r) => ok(r), - Err(e) => err(DfxError::SerdeCbor(e)), + Err(e) => err(DfxError::SerdeCborFromServer(e, hex::encode(&buf))), }) } @@ -195,6 +194,19 @@ pub fn query( ) } +/// Canister request status call +/// +/// When sending a call, the client will not return the result of the call. We need to poll the +/// client with this method, using the Request ID generated from the call. +/// +/// This function does not poll, this is left to the user to set a timeout and a polling strategy. +pub fn request_status( + client: Client, + request_id: RequestId, +) -> impl Future, Error = DfxError> { + read(client, ReadRequest::RequestStatus { request_id }) +} + /// Canister Install call pub fn install_code( client: Client, @@ -230,22 +242,21 @@ pub fn call( 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| { +) -> impl Future { + let request = SubmitRequest::Call { + canister_id, + method_name, + arg: arg.unwrap_or_else(|| Blob(vec![])), + nonce: Some(random_blob()), + }; + let request_id = to_request_id(&request).map_err(DfxError::from); + + submit(client, request).and_then(|response| { result( response .error_for_status() - .map(|_| ()) - .map_err(DfxError::from), + .map_err(DfxError::from) + .and_then(|_| request_id), ) }) } @@ -273,9 +284,8 @@ mod tests { #[test] fn query_request_serialization() { use serde_cbor::Value; - use std::convert::TryInto; - let canister_id = 1; + let canister_id = CanisterId::from(1); let method_name = "main".to_string(); let arg = Blob(vec![]); @@ -295,10 +305,8 @@ mod tests { Value::Text("request_type".to_string()), Value::Text("query".to_string()), ), - ( - Value::Text("canister_id".to_string()), - Value::Integer(canister_id.try_into().unwrap()), - ), + // TODO: when the client moves to using Blobs, move this to being a blob. + (Value::Text("canister_id".to_string()), Value::Integer(1)), ( Value::Text("method_name".to_string()), Value::Text(method_name.clone()), @@ -341,9 +349,9 @@ mod tests { serde_cbor::from_slice(&serde_cbor::to_vec(&response).unwrap()).unwrap(); let expected = ReadResponse::Replied { - reply: QueryResponseReply { + reply: Some(QueryResponseReply { arg: Blob(arg.clone()), - }, + }), }; assert_eq!(actual, expected); @@ -354,9 +362,9 @@ mod tests { let _ = env_logger::try_init(); let response = ReadResponse::Replied { - reply: QueryResponseReply { + reply: Some(QueryResponseReply { arg: Blob(Vec::from("Hello World")), - }, + }), }; let _m = mock("POST", "/api/v1/read") @@ -369,7 +377,12 @@ mod tests { url: mockito::server_url(), }); - let query = query(client, 1, "main".to_string(), Some(Blob(vec![]))); + let query = query( + client, + CanisterId::from(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); @@ -394,6 +407,7 @@ mod tests { Value::Text("status".to_string()), Value::Text("rejected".to_string()), ), + // TODO: when the client moves to using Blobs, move this to being a blob. (Value::Text("reject_code".to_string()), Value::Integer(1)), ( Value::Text("reject_message".to_string()), @@ -434,7 +448,12 @@ mod tests { url: mockito::server_url(), }); - let query = query(client, 1, "main".to_string(), Some(Blob(vec![]))); + let query = query( + client, + CanisterId::from(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); @@ -450,9 +469,8 @@ mod tests { #[test] fn install_code_request_serialization() { use serde_cbor::Value; - use std::convert::TryInto; - let canister_id = 1; + let canister_id = CanisterId::from(1); let module = Blob(vec![1]); let arg = Blob(vec![2]); @@ -471,10 +489,8 @@ mod tests { Value::Text("request_type".to_string()), Value::Text("install_code".to_string()), ), - ( - Value::Text("canister_id".to_string()), - Value::Integer(canister_id.try_into().unwrap()), - ), + // TODO: when the client moves to using Blobs, move this to being a blob. + (Value::Text("canister_id".to_string()), Value::Integer(1)), (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), @@ -499,7 +515,7 @@ mod tests { url: mockito::server_url(), }); - let future = install_code(client, 1, Blob(vec![1]), None); + let future = install_code(client, CanisterId::from(1), Blob(vec![1]), None); let mut runtime = tokio::runtime::Runtime::new().expect("Unable to create a runtime"); let result = runtime.block_on(future); @@ -525,7 +541,7 @@ mod tests { url: mockito::server_url(), }); - let future = install_code(client, 1, Blob(vec![1]), None); + let future = install_code(client, CanisterId::from(1), Blob(vec![1]), None); let mut runtime = tokio::runtime::Runtime::new().expect("Unable to create a runtime"); let result = runtime.block_on(future); diff --git a/dfx/src/lib/error.rs b/dfx/src/lib/error.rs index db096c0d2d..e6d2b6b7fa 100644 --- a/dfx/src/lib/error.rs +++ b/dfx/src/lib/error.rs @@ -1,4 +1,5 @@ use crate::lib::api_client::ReadRejectCode; +use ic_http_agent::{RequestIdError, RequestIdFromStringError}; #[derive(Debug)] pub enum BuildErrorKind { @@ -13,11 +14,14 @@ pub enum DfxError { IO(std::io::Error), ParseInt(std::num::ParseIntError), Reqwest(reqwest::Error), + SerdeCborFromServer(serde_cbor::error::Error, String), SerdeCbor(serde_cbor::error::Error), SerdeJson(serde_json::error::Error), Url(reqwest::UrlError), WabtError(wabt::Error), AddrParseError(std::net::AddrParseError), + HttpAgentError(RequestIdError), + RequestIdFromStringError(RequestIdFromStringError), /// An unknown command was used. The argument is the command itself. UnknownCommand(String), @@ -85,3 +89,15 @@ impl From for DfxError { DfxError::AddrParseError(err) } } + +impl From for DfxError { + fn from(err: RequestIdError) -> DfxError { + DfxError::HttpAgentError(err) + } +} + +impl From for DfxError { + fn from(err: RequestIdFromStringError) -> DfxError { + DfxError::RequestIdFromStringError(err) + } +} diff --git a/dfx/src/lib/mod.rs b/dfx/src/lib/mod.rs index c80c072ddd..ddd65a8889 100644 --- a/dfx/src/lib/mod.rs +++ b/dfx/src/lib/mod.rs @@ -2,5 +2,3 @@ pub mod api_client; pub mod env; pub mod error; pub mod webserver; - -pub type CanisterId = u64; diff --git a/dfx/src/lib/webserver.rs b/dfx/src/lib/webserver.rs index 1e91768bed..781192d4e5 100644 --- a/dfx/src/lib/webserver.rs +++ b/dfx/src/lib/webserver.rs @@ -73,5 +73,8 @@ pub fn webserver( serve_dir: &Path, ) -> std::thread::JoinHandle<()> { let serve_dir = PathBuf::from(serve_dir); - std::thread::spawn(move || run_webserver(bind, client_api_uri, serve_dir).unwrap()) + std::thread::Builder::new() + .name("Frontend".into()) + .spawn(move || run_webserver(bind, client_api_uri, serve_dir).unwrap()) + .unwrap() } diff --git a/dfx/src/util/clap/validators.rs b/dfx/src/util/clap/validators.rs index d1aef12828..2d9657b5a4 100644 --- a/dfx/src/util/clap/validators.rs +++ b/dfx/src/util/clap/validators.rs @@ -1,7 +1,24 @@ -use crate::lib::CanisterId; +use ic_http_agent::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_err(|_| String::from("The value must be a canister ID.")) .map(|_| ()) } + +pub fn is_request_id(v: String) -> Result<(), String> { + // A valid Request Id starts with `0x` and is a series of 64 hexadecimals. + if !v.starts_with("0x") { + Err(String::from("A Request ID needs to start with 0x.")) + } else if v.len() != 66 { + Err(String::from( + "A Request ID is 64 hexadecimal prefixed with 0x.", + )) + } else if v.as_str()[2..].contains(|c: char| !c.is_ascii_hexdigit()) { + Err(String::from( + "A Request ID is 64 hexadecimal prefixed with 0x. An invalid character was found.", + )) + } else { + Ok(()) + } +} diff --git a/e2e/assets/counter_as/counter.as b/e2e/assets/counter_as/counter.as new file mode 100644 index 0000000000..99d8a4096b --- /dev/null +++ b/e2e/assets/counter_as/counter.as @@ -0,0 +1,44 @@ +// This file defines a single actor (object) named Counter. +// +// Internally, Counter holds a natural number, its current count. + +// Public Spec of Counter actor +// ---------------------------- + +// +// To describe Counter's external behavior, we refer to +// its internal count, a natural number, as `cell`: +// +// - the `inc` message increments `cell`, with unit response value +// +// - the `read` message requests the value held in `cell` in response +// + +// An implementation of the Counter actor +// ---------------------------------------------------------------------- +// +// See below. +// Uses fewer than 10 lines of ActorScript, and many are just closing braces. +// +// Aside: to make checking the spec above as easy as possible, we +// intentionally reuse the name `cell` mentioned there for the single +// mutable variable in the implementation below; we didn't have to do +// this, of course, since the spec is technically independent of these +// implementation details: + +actor Counter { + + // our single variable, `cell`, holds the current count + var cell : Nat = 0; + + // increment `cell`, with a unit-valued (asynchronous) response + public func inc() : async () { + cell += 1; + }; + + // request the value held in `cell` in (asynchronous) response + public func read() : async Nat { + cell + }; + +} diff --git a/e2e/assets/counter_as/dfinity.json b/e2e/assets/counter_as/dfinity.json new file mode 100644 index 0000000000..e07e561d9d --- /dev/null +++ b/e2e/assets/counter_as/dfinity.json @@ -0,0 +1,21 @@ +{ + "// TODO": "Replace this file with `dfx config`.", + "version": 1, + "dfx": "0.2.0-local-debug", + "canisters": { + "hello": { + "main": "counter.as", + "canister_id": 42 + } + }, + "defaults": { + "build": { + "output": "build/" + }, + "start": { + "port": 8000, + "address": "127.0.0.1", + "serve_root": "app/" + } + } +} diff --git a/e2e/assets/counter_wat/counter.wat b/e2e/assets/counter_wat/counter.wat new file mode 100644 index 0000000000..5121f7d4bf --- /dev/null +++ b/e2e/assets/counter_wat/counter.wat @@ -0,0 +1,39 @@ +;; Counter with global variable ;; +(module + (import "msg" "reply" + (func $msg_reply (param $nonce i64) (param i32) (param i32))) + + (func $read (param $nonce i64) + (i32.store + (i32.const 0) + (global.get 0) + ) + (call $msg_reply + (local.get $nonce) + (i32.const 0) + (i32.const 4) + ) + ) + + (func $write (param $nonce i64) + (global.set 0 + (i32.add + (global.get 0) + (i32.const 1) + ) + ) + ) + + ;; Both increments and reads + (func $inc_read (param $nonce i64) + (call $write (local.get $nonce)) + (call $read (local.get $nonce)) + ) + + (memory $memory 1) + (export "memory" (memory $memory)) + (global (mut i32) (i32.const 0)) + (export "canister_query read" (func $read)) + (export "canister_query inc_read" (func $inc_read)) + (export "canister_update write" (func $write)) +) diff --git a/e2e/assets/counter_wat/dfinity.json b/e2e/assets/counter_wat/dfinity.json new file mode 100644 index 0000000000..0e37515aa6 --- /dev/null +++ b/e2e/assets/counter_wat/dfinity.json @@ -0,0 +1,21 @@ +{ + "// TODO": "Replace this file with `dfx config`.", + "version": 1, + "dfx": "0.2.0-local-debug", + "canisters": { + "hello": { + "main": "counter.wat", + "canister_id": 42 + } + }, + "defaults": { + "build": { + "output": "build/" + }, + "start": { + "port": 8000, + "address": "127.0.0.1", + "serve_root": "app/" + } + } +} diff --git a/e2e/basic-project.bash b/e2e/basic-project.bash new file mode 100644 index 0000000000..53f92a9b0a --- /dev/null +++ b/e2e/basic-project.bash @@ -0,0 +1,149 @@ +#!/usr/bin/env bats + +setup() { + # We want to work from a temporary directory, different for every test. + cd $(mktemp -d -t dfx-e2e-XXXXXXXX) +} + +teardown() { + # Kill the node manager, the dfx and the client. Ignore errors (ie. if processes aren't + # running). + killall dfx nodemanager client || true +} + +# Create a new project and starts its client in the background. +dfx_start() { + # Bats create a FD 3 for test output, but child processes inherit it and Bats will + # wait for it to close. Because `dfx start` leave a child process running, we need + # to close this pipe, otherwise Bats will wait indefinitely. + dfx start --background 3>&- +} + +@test "dfx new succeeds" { + dfx new e2e-project + + test -d e2e-project + test -f e2e-project/dfinity.json +} + +@test "canister query -- greet" { + dfx new e2e-project + cd e2e-project + dfx_start + + run dfx canister query 42 greet Banzai + echo $output + [[ $status == 0 ]] + [[ "$output" == "Hello, Banzai!" ]] +} + +@test "canister call wait -- greet" { + dfx new e2e-project + cd e2e-project + dfx_start + + run dfx canister call --wait 42 greet Bongalo + echo $output + [[ $status == 0 ]] + [[ "$output" == "Hello, Bongalo!" ]] +} + +@test "canister call + request-status -- greet" { + dfx new e2e-project + cd e2e-project + dfx_start + + run dfx canister call 42 greet Bongalo + [[ $status == 0 ]] + + run dfx canister request-status $output + [[ $status == 0 ]] + [[ "$output" == "Hello, Bongalo!" ]] +} + +@test "build + install + call + request-status -- counter_wat" { + dfx new e2e-project + cd e2e-project + + cp ${BATS_TEST_DIRNAME}/assets/counter_wat/* . + + dfx build + dfx_start + dfx canister install 42 build/counter.wasm + + # Currently the counter is set to 0. We call write which increments it + # 64 times. This is important because query returns a byte, and 64 is + # "A" in UTF8. We then just compare and work around the alphabet. + for _x in {0..64}; do + dfx canister call --wait 42 write + done + + run dfx canister query 42 read + [[ "$output" == "A" ]] + run dfx canister query 42 read + [[ "$output" == "A" ]] + + dfx canister call --wait 42 write + run dfx canister query 42 read + [[ "$output" == "B" ]] + + dfx canister call --wait 42 write + run dfx canister query 42 read + [[ "$output" == "C" ]] + + run dfx canister call 42 write + [[ $status == 0 ]] + dfx canister request-status $output + [[ $status == 0 ]] + + # Write has no return value. But we can _call_ read too. + run dfx canister call 42 read + [[ $status == 0 ]] + run dfx canister request-status $output + [[ $status == 0 ]] + [[ "$output" == "D" ]] +} + +@test "build + install + call + request-status -- counter_as" { + skip "This does not work as the AS tries to deserialize IDL, which we dont support yet." + dfx new e2e-project + cd e2e-project + + cp ${BATS_TEST_DIRNAME}/assets/counter_as/* . + + dfx build + dfx_start + dfx canister install 42 build/counter.wasm + + # Currently the counter is set to 0. We call write which increments it + # 64 times. This is important because query returns a byte, and 64 is + # "A" in UTF8. We then just compare and work around the alphabet. + for _x in {0..64}; do + dfx canister call --wait 42 inc + done + + run dfx canister query 42 read + [[ "$output" == "A" ]] + run dfx canister query 42 read + [[ "$output" == "A" ]] + + dfx canister call --wait 42 inc + run dfx canister query 42 read + [[ "$output" == "B" ]] + + dfx canister call --wait 42 inc + run dfx canister query 42 read + [[ "$output" == "C" ]] + + run dfx canister call 42 inc + [[ $status == 0 ]] + dfx canister request-status $output + [[ $status == 0 ]] + + # Write has no return value. But we can _call_ read too. + run dfx canister call 42 read + [[ $status == 0 ]] + run dfx canister request-status $output + [[ $status == 0 ]] + [[ "$output" == "D" ]] +} diff --git a/e2e/usage.bash b/e2e/usage.bash new file mode 100644 index 0000000000..7be7617c06 --- /dev/null +++ b/e2e/usage.bash @@ -0,0 +1,17 @@ +#!/usr/bin/env bats + +@test "dfx help succeeds" { + dfx --help +} + +@test "dfx help contains new command" { + dfx --help | grep new +} + +@test "using an invalid command fails" { + run dfx blurp + if [[ $status == 0 ]]; then + echo $@ >&2 + exit 1 + fi +} diff --git a/lib/ic_http_agent/Cargo.toml b/lib/ic_http_agent/Cargo.toml new file mode 100644 index 0000000000..dd6e9f8d2d --- /dev/null +++ b/lib/ic_http_agent/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ic-http-agent" +version = "0.1.0" +authors = ["Hans Larsen "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +byteorder = "1.3.2" +error = "0.1.9" +hex = "0.4.0" +openssl = "0.10.24" +reqwest = "0.9.20" +serde = "1.0.101" +serde_bytes = "0.11.2" +serde_cbor = "0.10.1" + +[dev-dependencies] +rand = "0.7.2" diff --git a/lib/ic_http_agent/README.adoc b/lib/ic_http_agent/README.adoc new file mode 100644 index 0000000000..1adc848631 --- /dev/null +++ b/lib/ic_http_agent/README.adoc @@ -0,0 +1,9 @@ += Public Spec + +== Goal +This library contains typings and utility functions dealing with the public spec and the HTTP +client. It might be shared in the future but for now is separated for the purpose of testing and +development. + +== References +The latest version of the https://hydra.dfinity.systems/latest/dfinity-ci-build/dfinity/dfinity.docs.x86_64-linux/dfinity/spec/public/index.html[public spec] is available internally on hydra. diff --git a/lib/ic_http_agent/src/lib.rs b/lib/ic_http_agent/src/lib.rs new file mode 100644 index 0000000000..3dc47a01e3 --- /dev/null +++ b/lib/ic_http_agent/src/lib.rs @@ -0,0 +1,2 @@ +mod types; +pub use types::public::*; diff --git a/lib/ic_http_agent/src/types/blob.rs b/lib/ic_http_agent/src/types/blob.rs new file mode 100644 index 0000000000..2dc440fcc0 --- /dev/null +++ b/lib/ic_http_agent/src/types/blob.rs @@ -0,0 +1,71 @@ +use crate::types::request_id; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; + +#[cfg(test)] +use rand::{thread_rng, RngCore}; + +/// A binary "blob", i.e. a byte array +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Blob(pub Vec); + +impl Blob { + #[cfg(test)] + pub fn random(size: usize) -> Blob { + let mut rng = thread_rng(); + let mut v: Vec = Vec::with_capacity(size); + rng.fill_bytes(v.as_mut_slice()); + + Blob(v) + } +} + +impl From<&[u8]> for Blob { + fn from(a: &[u8]) -> Blob { + Blob(a.to_vec()) + } +} + +/// Serialize into a u64 for now. +impl Serialize for Blob { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(self.0.as_slice()) + } +} + +/// Simple visitor for deserialization from bytes. We don't support other number types +/// as there's no need for it. +struct BlobVisitor; + +impl<'de> de::Visitor<'de> for BlobVisitor { + type Value = Blob; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a binary large object (bytes)") + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: de::Error, + { + Ok(Blob::from(value)) + } +} + +impl<'de> Deserialize<'de> for Blob { + fn deserialize(deserializer: S) -> Result + where + S: Deserializer<'de>, + { + deserializer.deserialize_bytes(BlobVisitor) + } +} + +impl From for Blob { + fn from(rid: request_id::RequestId) -> Blob { + Blob(rid.to_vec()) + } +} diff --git a/lib/ic_http_agent/src/types/canister_id.rs b/lib/ic_http_agent/src/types/canister_id.rs new file mode 100644 index 0000000000..5bdd775018 --- /dev/null +++ b/lib/ic_http_agent/src/types/canister_id.rs @@ -0,0 +1,115 @@ +use crate::types::blob::Blob; +use byteorder::{BigEndian, ByteOrder}; +use hex; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::{fmt, num, str}; + +/// A Canister ID. +/// +/// This type is described as a Blob in the public spec, but used as an integer in most +/// code samples (including this library). For now, we newtype it to abstract its usage +/// from a number, and will change its internal type when time comes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CanisterId(Blob); + +impl CanisterId { + pub(crate) fn from_u64(v: u64) -> CanisterId { + let mut buf = [0 as u8; 8]; + BigEndian::write_u64(&mut buf, v); + CanisterId(Blob(buf.to_vec())) + } + + pub(crate) fn as_u64(&self) -> u64 { + BigEndian::read_u64((self.0).0.as_slice()) + } + + /// Allow to move canister Ids in blobs. + pub fn into_blob(self) -> Blob { + self.0 + } + + pub fn from_hex>(h: S) -> Result { + Ok(CanisterId(Blob::from(hex::decode(h)?.as_slice()))) + } + + pub fn to_hex(&self) -> String { + hex::encode(&(self.0).0) + } +} + +/// Serialize into a blob. +impl Serialize for CanisterId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // TODO(DFN-862): move this to blobs + serializer.serialize_u64(self.as_u64()) + } +} + +impl<'de> Deserialize<'de> for CanisterId { + fn deserialize(deserializer: S) -> Result + where + S: Deserializer<'de>, + { + // TODO(DFN-862): move this to blobs + Ok(CanisterId::from_u64(u64::deserialize(deserializer)?)) + } +} + +/// Conversion of different types that should be coerce-able to Canister Ids. +impl From for CanisterId { + fn from(b: Blob) -> CanisterId { + // We don't need to make a copy as this assume ownership. + CanisterId(b) + } +} + +impl From for CanisterId { + fn from(n: u64) -> CanisterId { + // We don't need to make a copy as this assume ownership. + CanisterId::from_u64(n) + } +} + +impl str::FromStr for CanisterId { + type Err = num::ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(CanisterId::from_u64(u64::from_str(s)?)) + } +} + +impl fmt::Display for CanisterId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "canister({})", self.to_hex()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_serialize_deserialize() { + let id = CanisterId::from_u64(88827); + + // Use cbor serialization. + let vec = serde_cbor::to_vec(&id).unwrap(); + let value = serde_cbor::from_slice(vec.as_slice()).unwrap(); + + assert_eq!(id, value); + } + + #[test] + fn hex_encode() { + let cid: CanisterId = CanisterId::from(Blob::from(vec![1, 8, 64, 255].as_slice())); + + let hex = cid.to_hex(); + let cid2 = CanisterId::from_hex(&hex).unwrap(); + + assert_eq!(cid, cid2); + assert_eq!(hex, "010840ff"); + } +} diff --git a/lib/ic_http_agent/src/types/mod.rs b/lib/ic_http_agent/src/types/mod.rs new file mode 100644 index 0000000000..65503cac20 --- /dev/null +++ b/lib/ic_http_agent/src/types/mod.rs @@ -0,0 +1,13 @@ +pub(crate) mod blob; +pub(crate) mod canister_id; +pub(crate) mod request_id; +pub(crate) mod request_id_error; + +pub(crate) mod public { + use super::*; + + pub use blob::Blob; + pub use canister_id::CanisterId; + pub use request_id::{to_request_id, RequestId}; + pub use request_id_error::{RequestIdError, RequestIdFromStringError}; +} diff --git a/lib/ic_http_agent/src/types/request_id.rs b/lib/ic_http_agent/src/types/request_id.rs new file mode 100644 index 0000000000..e485b964b2 --- /dev/null +++ b/lib/ic_http_agent/src/types/request_id.rs @@ -0,0 +1,702 @@ +//! This module deals with computing Request IDs based on the content of a +//! message. +//! +//! We compute the `RequestId` according to the public spec, which +//! specifies it as a "sha256" digest. +//! +//! A single method is exported, to_request_id, which returns a RequestId +//! (a 256 bits slice) or an error. +use crate::types::request_id_error::{RequestIdError, RequestIdFromStringError}; +use byteorder::{BigEndian, ByteOrder}; +use openssl::sha::Sha256; +use serde::{ser, Serialize, Serializer}; +use std::collections::BTreeMap; +use std::iter::Extend; +use std::str::FromStr; + +/// Type alias for a sha256 result (ie. a u256). +type Sha256Hash = [u8; 32]; + +/// A Request ID. +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub struct RequestId(Sha256Hash); + +impl RequestId { + pub fn new(from: &[u8; 32]) -> RequestId { + RequestId(*from) + } + + pub(crate) fn to_vec(&self) -> Vec { + self.0.to_vec() + } +} + +impl FromStr for RequestId { + type Err = RequestIdFromStringError; + + fn from_str(from: &str) -> Result { + let mut blob: [u8; 32] = [0; 32]; + let vec = hex::decode(from).map_err(RequestIdFromStringError::FromHexError)?; + if vec.len() != 32 { + return Err(RequestIdFromStringError::InvalidSize(vec.len())); + } + + blob.copy_from_slice(vec.as_slice()); + Ok(RequestId::new(&blob)) + } +} + +impl From for String { + fn from(id: RequestId) -> String { + hex::encode(id.0) + } +} + +/// We only allow to serialize a Request ID. +impl Serialize for RequestId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&self.to_vec()) + } +} + +/// A Serde Serializer that collects fields and values in order to hash them later. +/// We serialize the type to this structure, then use the trait to hash its content. +/// It is a simple state machine that contains 3 states: +/// 1. The root value, which is a structure. If a value other than a structure is +/// serialized, this errors. This is determined by whether `fields` is Some(_). +/// 2. The structure is being processed, and the value of a field is being +/// serialized. The field_value_hash will be set to Some(_). +/// 3. The finish() function has been called and the hasher cannot be reused. The +/// hash should have been gotten at this point. +/// +/// Inconsistent state are when a field is being serialized and `fields` is None, or +/// when a value (not struct) is being serialized and field_value_hash is None. +/// +/// This will always fail on types that are unknown to the Request format (e.g. i8). +/// An UnsupportedTypeXXX error will be returned. +/// +/// The only types that are supported right now are: +/// . Strings and string slices. +/// . Blobs (the newtype exported from this crate). +/// . A structure as the base level. Its typename and fields are not validated. +/// +/// Additionally, this will fail if there are unsupported data structure, for example +/// if a UnitVariant of another type than Blob is used, or a structure inside a +/// structure. +/// +/// This does not validate whether a message is valid. This is very important as +/// the message format might change faster than the ID calculation. +struct RequestIdSerializer { + // We use a BTreeMap here as there is no indication that keys might not be duplicated, + // and we want to make sure they're overwritten in that case. + fields: Option>, + field_key_hash: Option, // Only used in maps, not structs. + field_value_hash: Option, + hasher: Sha256, +} + +impl RequestIdSerializer { + pub fn new() -> RequestIdSerializer { + Default::default() + } + + /// Finish the hashing and returns the RequestId for the structure that was + /// serialized. + /// + /// This can only be called once (it borrows self). Since this whole class is not public, + /// it should not be a problem. + pub fn finish(mut self) -> Result { + if self.fields.is_some() { + self.fields = None; + Ok(RequestId(self.hasher.finish())) + } else { + Err(RequestIdError::EmptySerializer) + } + } + + /// Hash a single value, returning its sha256_hash. If there is already a value + /// being hashed it will return an InvalidState. This cannot happen currently + /// as we don't allow embedded structures, but is left as a safeguard when + /// making changes. + fn hash_value(&mut self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + if self.field_value_hash.is_some() { + return Err(RequestIdError::InvalidState); + } + + self.field_value_hash = Some(Sha256::new()); + value.serialize(&mut *self)?; + if let Some(r) = self.field_value_hash.take() { + Ok(r.finish()) + } else { + Err(RequestIdError::InvalidState) + } + } +} + +impl Default for RequestIdSerializer { + fn default() -> RequestIdSerializer { + RequestIdSerializer { + fields: None, + field_key_hash: None, + field_value_hash: None, + hasher: Sha256::new(), + } + } +} + +/// See https://serde.rs/data-format.html for more information on how to implement a +/// custom data format. +impl<'a> ser::Serializer for &'a mut RequestIdSerializer { + /// The output type produced by this `Serializer` during successful + /// serialization. Most serializers that produce text or binary output + /// should set `Ok = ()` and serialize into an [`io::Write`] or buffer + /// contained within the `Serializer` instance. Serializers that build + /// in-memory data structures may be simplified by using `Ok` to propagate + /// the data structure around. + /// + /// [`io::Write`]: https://doc.rust-lang.org/std/io/trait.Write.html + type Ok = (); + + /// The error type when some error occurs during serialization. + type Error = RequestIdError; + + // Associated types for keeping track of additional state while serializing + // compound data structures like sequences and maps. In this case no + // additional state is required beyond what is already stored in the + // Serializer struct. + type SerializeSeq = Self; + type SerializeTuple = Self; + type SerializeTupleStruct = Self; + type SerializeTupleVariant = Self; + type SerializeMap = Self; + type SerializeStruct = Self; + type SerializeStructVariant = Self; + + /// Serialize a `bool` value. + fn serialize_bool(self, _v: bool) -> Result { + Err(RequestIdError::UnsupportedTypeBool) + } + + /// Serialize an `i8` value. + fn serialize_i8(self, _v: i8) -> Result { + Err(RequestIdError::UnsupportedTypeI8) + } + + /// Serialize an `i16` value. + fn serialize_i16(self, _v: i16) -> Result { + Err(RequestIdError::UnsupportedTypeI16) + } + + /// Serialize an `i32` value. + fn serialize_i32(self, _v: i32) -> Result { + Err(RequestIdError::UnsupportedTypeI32) + } + + /// Serialize an `i64` value. + fn serialize_i64(self, _v: i64) -> Result { + Err(RequestIdError::UnsupportedTypeI64) + } + + /// Serialize a `u8` value. + fn serialize_u8(self, _v: u8) -> Result { + Err(RequestIdError::UnsupportedTypeU8) + } + + /// Serialize a `u16` value. + fn serialize_u16(self, _v: u16) -> Result { + Err(RequestIdError::UnsupportedTypeU16) + } + + /// Serialize a `u32` value. + fn serialize_u32(self, _v: u32) -> Result { + Err(RequestIdError::UnsupportedTypeU32) + } + + /// Serialize a `u64` value. + fn serialize_u64(self, v: u64) -> Result { + let mut buf = [0 as u8; 8]; + BigEndian::write_u64(&mut buf, v); + self.serialize_bytes(&buf) + } + + /// Serialize an `f32` value. + fn serialize_f32(self, _v: f32) -> Result { + Err(RequestIdError::UnsupportedTypeF32) + } + + /// Serialize an `f64` value. + fn serialize_f64(self, _v: f64) -> Result { + Err(RequestIdError::UnsupportedTypeF64) + } + + /// Serialize a character. + fn serialize_char(self, _v: char) -> Result { + Err(RequestIdError::UnsupportedTypeChar) + } + + /// Serialize a `&str`. + fn serialize_str(self, v: &str) -> Result { + self.serialize_bytes(v.as_bytes()) + } + + /// Serialize a chunk of raw byte data. + fn serialize_bytes(self, v: &[u8]) -> Result { + match self.field_value_hash { + None => Err(RequestIdError::InvalidState), + Some(ref mut hash) => { + (*hash).update(v); + Ok(()) + } + } + } + + /// Serialize a [`None`] value. + fn serialize_none(self) -> Result { + // Compute the hash as if it was empty string or blob. + match self.field_value_hash { + None => Err(RequestIdError::InvalidState), + Some(ref mut _hash) => Ok(()), + } + } + + /// Serialize a [`Some(T)`] value. + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + // Compute the hash as if it was the value itself. + value.serialize(self) + } + + /// Serialize a `()` value. + fn serialize_unit(self) -> Result { + Err(RequestIdError::UnsupportedTypeUnit) + } + + /// Serialize a unit struct like `struct Unit` or `PhantomData`. + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(RequestIdError::UnsupportedTypePhantomData) + } + + /// Serialize a unit variant like `E::A` in `enum E { A, B }`. + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + Err(RequestIdError::UnsupportedTypeUnitVariant) + } + + /// Serialize a newtype struct like `struct Millimeters(u8)`. + fn serialize_newtype_struct( + self, + name: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + match name { + "Blob" => value.serialize(self), // value is of type Vec. + v => Err(RequestIdError::UnsupportedTypeNewtypeStruct(v.to_owned())), + } + } + + /// Serialize a newtype variant like `E::N` in `enum E { N(u8) }`. + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + Err(RequestIdError::UnsupportedTypeNewTypeVariant) + } + + /// Begin to serialize a variably sized sequence. This call must be + /// followed by zero or more calls to `serialize_element`, then a call to + /// `end`. + fn serialize_seq(self, _len: Option) -> Result { + Ok(self) + } + + /// Begin to serialize a statically sized sequence whose length will be + /// known at deserialization time without looking at the serialized data. + /// This call must be followed by zero or more calls to `serialize_element`, + /// then a call to `end`. + fn serialize_tuple(self, _len: usize) -> Result { + Err(RequestIdError::UnsupportedTypeTuple) + } + + /// Begin to serialize a tuple struct like `struct Rgb(u8, u8, u8)`. This + /// call must be followed by zero or more calls to `serialize_field`, then a + /// call to `end`. + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(RequestIdError::UnsupportedTypeTupleStruct) + } + + /// Begin to serialize a tuple variant like `E::T` in `enum E { T(u8, u8) + /// }`. This call must be followed by zero or more calls to + /// `serialize_field`, then a call to `end`. + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(RequestIdError::UnsupportedTypeTupleVariant) + } + + /// Begin to serialize a map. This call must be followed by zero or more + /// calls to `serialize_key` and `serialize_value`, then a call to `end`. + fn serialize_map(self, _len: Option) -> Result { + // This is the same as struct, but unnamed. We will use the current_field field + // here though, as serialize key and value are separate functions. + if self.fields.is_none() { + self.fields = Some(BTreeMap::new()); + Ok(self) + } else { + Err(RequestIdError::UnsupportedStructInsideStruct) + } + } + + /// Begin to serialize a struct like `struct Rgb { r: u8, g: u8, b: u8 }`. + /// This call must be followed by zero or more calls to `serialize_field`, + /// then a call to `end`. + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + if self.fields.is_none() { + self.fields = Some(BTreeMap::new()); + Ok(self) + } else { + Err(RequestIdError::UnsupportedStructInsideStruct) + } + } + + /// Begin to serialize a struct variant like `E::S` in `enum E { S { r: u8, + /// g: u8, b: u8 } }`. This call must be followed by zero or more calls to + /// `serialize_field`, then a call to `end`. + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(RequestIdError::UnsupportedTypeStructVariant) + } +} + +// The following 7 impls deal with the serialization of compound types like +// sequences and maps. Serialization of such types is begun by a Serializer +// method and followed by zero or more calls to serialize individual elements of +// the compound type and one call to end the compound type. +// +// This impl is SerializeSeq so these methods are called after `serialize_seq` +// is called on the Serializer. +impl<'a> ser::SerializeSeq for &'a mut RequestIdSerializer { + // Must match the `Ok` type of the serializer. + type Ok = (); + // Must match the `Error` type of the serializer. + type Error = RequestIdError; + + // Serialize a single element of the sequence. + fn serialize_element(&mut self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(&mut **self) + } + + // Close the sequence. + fn end(self) -> Result { + Ok(()) + } +} + +// Same thing but for tuples. +impl<'a> ser::SerializeTuple for &'a mut RequestIdSerializer { + type Ok = (); + type Error = RequestIdError; + + fn serialize_element(&mut self, _value: &T) -> Result + where + T: ?Sized + Serialize, + { + Err(RequestIdError::Custom( + "Unsupported field type: SerializeTuple element.".to_string(), + )) + } + + fn end(self) -> Result { + Ok(()) + } +} + +// Same thing but for tuple structs. +impl<'a> ser::SerializeTupleStruct for &'a mut RequestIdSerializer { + type Ok = (); + type Error = RequestIdError; + + fn serialize_field(&mut self, _value: &T) -> Result + where + T: ?Sized + Serialize, + { + Err(RequestIdError::Custom( + "Unsupported field type: SerializeTupleStruct field.".to_string(), + )) + } + + fn end(self) -> Result { + Ok(()) + } +} + +// Tuple variants are a little different. Refer back to the +// `serialize_tuple_variant` method above: +// +// self.output += "{"; +// variant.serialize(&mut *self)?; +// self.output += ":["; +// +// So the `end` method in this impl is responsible for closing both the `]` and +// the `}`. +impl<'a> ser::SerializeTupleVariant for &'a mut RequestIdSerializer { + type Ok = (); + type Error = RequestIdError; + + fn serialize_field(&mut self, _value: &T) -> Result + where + T: ?Sized + Serialize, + { + Err(RequestIdError::Custom( + "Unsupported field type: SerializeTupleVariant field.".to_string(), + )) + } + + fn end(self) -> Result { + Ok(()) + } +} + +// Some `Serialize` types are not able to hold a key and value in memory at the +// same time so `SerializeMap` implementations are required to support +// `serialize_key` and `serialize_value` individually. +// +// There is a third optional method on the `SerializeMap` trait. The +// `serialize_entry` method allows serializers to optimize for the case where +// key and value are both available simultaneously. In JSON it doesn't make a +// difference so the default behavior for `serialize_entry` is fine. +impl<'a> ser::SerializeMap for &'a mut RequestIdSerializer { + type Ok = (); + type Error = RequestIdError; + + // The Serde data model allows map keys to be any serializable type. JSON + // only allows string keys so the implementation below will produce invalid + // JSON if the key serializes as something other than a string. + // + // A real JSON serializer would need to validate that map keys are strings. + // This can be done by using a different Serializer to serialize the key + // (instead of `&mut **self`) and having that other serializer only + // implement `serialize_str` and return an error on any other data type. + fn serialize_key(&mut self, key: &T) -> Result + where + T: ?Sized + Serialize, + { + if self.field_key_hash.is_some() { + Err(RequestIdError::InvalidState) + } else { + let key_hash = self.hash_value(key)?; + self.field_key_hash = Some(key_hash); + Ok(()) + } + } + + // It doesn't make a difference whether the colon is printed at the end of + // `serialize_key` or at the beginning of `serialize_value`. In this case + // the code is a bit simpler having it here. + fn serialize_value(&mut self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + let value_hash = self.hash_value(value)?; + + match self.field_key_hash.take() { + None => Err(RequestIdError::InvalidState), + Some(key_hash) => match self.fields { + None => Err(RequestIdError::InvalidState), + Some(ref mut f) => { + f.insert(key_hash, value_hash); + Ok(()) + } + }, + } + } + + fn end(self) -> Result { + Ok(()) + } +} + +// Structs are like maps in which the keys are constrained to be compile-time +// constant strings. +impl<'a> ser::SerializeStruct for &'a mut RequestIdSerializer { + type Ok = (); + type Error = RequestIdError; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result + where + T: ?Sized + Serialize, + { + if self.field_value_hash.is_some() { + return Err(RequestIdError::InvalidState); + } + + let key_hash = self.hash_value(key)?; + let value_hash = self.hash_value(value)?; + + match self.fields { + None => Err(RequestIdError::InvalidState), + Some(ref mut f) => { + f.insert(key_hash, value_hash); + Ok(()) + } + } + } + + fn end(self) -> Result { + if let Some(fields) = &self.fields { + // Sort the fields. + let mut keyvalues: Vec> = fields + .keys() + .zip(fields.values()) + .map(|(k, v)| { + let mut x = k.to_vec(); + x.extend(v); + x + }) + .collect(); + keyvalues.sort(); + + for kv in keyvalues { + self.hasher.update(&kv); + } + + Ok(()) + } else { + Err(RequestIdError::InvalidState) + } + } +} + +// Similar to `SerializeTupleVariant`, here the `end` method is responsible for +// closing both of the curly braces opened by `serialize_struct_variant`. +impl<'a> ser::SerializeStructVariant for &'a mut RequestIdSerializer { + type Ok = (); + type Error = RequestIdError; + + fn serialize_field( + &mut self, + _key: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Err(RequestIdError::Custom( + "Unsupported field type: SerializeStructVariant field.".to_string(), + )) + } + + fn end(self) -> Result { + Ok(()) + } +} + +/// Derive the request ID from a serializable data structure. +/// +/// See https://hydra.dfinity.systems//build/268411/download/1/dfinity/spec/public/index.html#api-request-id +pub fn to_request_id<'a, V>(value: &V) -> Result +where + V: 'a + Serialize, +{ + let mut serializer = RequestIdSerializer::new(); + value.serialize(&mut serializer)?; + serializer.finish() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Blob, CanisterId}; + + /// The actual example used in the public spec in the Request ID section. + #[test] + fn public_spec_example() { + #[derive(Serialize)] + struct PublicSpecExampleStruct { + request_type: &'static str, + canister_id: CanisterId, + method_name: &'static str, + arg: Blob, + }; + let data = PublicSpecExampleStruct { + request_type: "call", + canister_id: CanisterId::from(1234), + method_name: "hello", + arg: Blob(b"DIDL\x00\xFD*".to_vec()), + }; + + // Hash taken from the example on the public spec. + let request_id = to_request_id(&data).unwrap(); + assert_eq!( + hex::encode(request_id.0.to_vec()), + "8781291c347db32a9d8c10eb62b710fce5a93be676474c42babc74c51858f94b" + ); + } + + /// The same example as above, except we use the ApiClient enum newtypes. + #[test] + fn public_spec_example_api_client() { + #[derive(Serialize)] + #[serde(rename_all = "snake_case")] + #[serde(tag = "request_type")] + enum PublicSpec { + Call { + canister_id: CanisterId, + method_name: String, + arg: Option, + }, + } + let data = PublicSpec::Call { + canister_id: CanisterId::from(1234), + method_name: "hello".to_owned(), + arg: Some(Blob(b"DIDL\x00\xFD*".to_vec())), + }; + + // Hash taken from the example on the public spec. + let request_id = to_request_id(&data).unwrap(); + assert_eq!( + hex::encode(request_id.0.to_vec()), + "8781291c347db32a9d8c10eb62b710fce5a93be676474c42babc74c51858f94b" + ); + } +} diff --git a/lib/ic_http_agent/src/types/request_id_error.rs b/lib/ic_http_agent/src/types/request_id_error.rs new file mode 100644 index 0000000000..d3ab95dc68 --- /dev/null +++ b/lib/ic_http_agent/src/types/request_id_error.rs @@ -0,0 +1,78 @@ +use serde::ser; +use std::error::Error; +use std::fmt::{Display, Formatter}; + +/// Errors from reading a RequestId from a string. This is not the same as +/// deserialization. +#[derive(Debug)] +pub enum RequestIdFromStringError { + InvalidSize(usize), + FromHexError(hex::FromHexError), +} + +/// An error during the calculation of the RequestId. +/// Since we use serde for serializing a data type into a hash, this has to support traits that +/// serde expects, such as Display +#[derive(Clone, Debug, PartialEq)] +pub enum RequestIdError { + Custom(String), + + EmptySerializer, + InvalidState, + UnsupportedStructInsideStruct, + + // Base types. + UnsupportedTypeBool, + UnsupportedTypeU8, + UnsupportedTypeU16, + UnsupportedTypeU32, + UnsupportedTypeU64, + UnsupportedTypeU128, + UnsupportedTypeI8, + UnsupportedTypeI16, + UnsupportedTypeI32, + UnsupportedTypeI64, + UnsupportedTypeI128, + UnsupportedTypeF32, + UnsupportedTypeF64, + UnsupportedTypeChar, + // UnsupportedTypeStr, // Supported + UnsupportedTypeBytes, + // UnsupportedTypeNone, // Supported + // UnsupportedTypeSome, // Supported + UnsupportedTypeUnit, + UnsupportedTypePhantomData, + + // Variants and complex types. + UnsupportedTypeUnitVariant, + UnsupportedTypeNewtypeStruct(String), + UnsupportedTypeNewTypeVariant, + UnsupportedTypeSequence, + UnsupportedTypeTuple, + UnsupportedTypeTupleStruct, + UnsupportedTypeTupleVariant, + UnsupportedTypeMap, + // UnsupportedTypeStruct, // Supported + UnsupportedTypeStructVariant, +} + +impl Display for RequestIdError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl Error for RequestIdError { + fn description(&self) -> &str { + "An error happened during request_id." + } +} + +impl ser::Error for RequestIdError { + fn custom(msg: T) -> Self + where + T: std::fmt::Display, + { + RequestIdError::Custom(msg.to_string()) + } +} diff --git a/nix/e2e-tests.nix b/nix/e2e-tests.nix new file mode 100644 index 0000000000..8faf180678 --- /dev/null +++ b/nix/e2e-tests.nix @@ -0,0 +1,16 @@ +{ bats +, coreutils +, dfinity-sdk +, runCommandNoCC +, stdenv +, killall +}: + +runCommandNoCC "e2e-tests" { + buildInputs = [ bats coreutils dfinity-sdk.packages.rust-workspace-debug stdenv.cc killall ]; +} '' + # We want $HOME/.cache to be in a new temporary directory. + HOME=$(mktemp -d -t dfx-e2e-home-XXXX) + + timeout --preserve-status 120 bats --recursive ${../e2e}/* | tee $out +'' diff --git a/nix/overlays/dfinity-sdk.nix b/nix/overlays/dfinity-sdk.nix index a7296fedd7..1cb08910f5 100644 --- a/nix/overlays/dfinity-sdk.nix +++ b/nix/overlays/dfinity-sdk.nix @@ -11,6 +11,7 @@ self: super: { name = "${oldAttrs.name}-debug"; }); rust-workspace-doc = rust-workspace-debug.doc; + e2e-tests = super.callPackage ../e2e-tests.nix {}; }; # This is to make sure CI evalutes shell derivations, builds their diff --git a/nix/overlays/dfinity.nix b/nix/overlays/dfinity.nix index 5e288db6c1..9d65cf41b4 100644 --- a/nix/overlays/dfinity.nix +++ b/nix/overlays/dfinity.nix @@ -3,9 +3,9 @@ self: super: let src = builtins.fetchGit { url = "ssh://git@github.com/dfinity-lab/dfinity"; # ref = "v0.2.0"; # TODO - rev = "93f86a53d6541b88b9427ae32fe492368b403afe"; + rev = "c14c1d8013d35d1894f6f186b1802616f46983de"; }; in { - dfinity = (import src {}).dfinity; + dfinity = (import src { inherit (self) system; }).dfinity; } diff --git a/nix/rust-workspace.nix b/nix/rust-workspace.nix index b650660454..aa43d36378 100644 --- a/nix/rust-workspace.nix +++ b/nix/rust-workspace.nix @@ -1,10 +1,17 @@ -{ buildDfinityRustPackage, stdenv, lib, darwin, libressl, cargo-graph, graphviz -#, cargo-graph, graphviz -, dfinity, actorscript, runCommandNoCC -, release ? true # is it a "release" build, as opposed to "debug" ? +{ release ? true , doClippy ? false , doFmt ? false , doDoc ? false +, actorscript +, buildDfinityRustPackage +, cargo-graph +, darwin +, dfinity +, graphviz +, lib +, libressl +, runCommandNoCC +, stdenv }: let drv = buildDfinityRustPackage { @@ -23,7 +30,7 @@ in drv.overrideAttrs (oldAttrs: { DFX_ASSETS = runCommandNoCC "dfx-assets" {} '' mkdir -p $out - cp ${dfinity.rust-workspace}/bin/{client,nodemanager} $out + cp ${if release then dfinity.rust-workspace else dfinity.rust-workspace-debug}/bin/{client,nodemanager} $out cp ${actorscript.asc}/bin/asc $out cp ${actorscript.as-ide}/bin/as-ide $out cp ${actorscript.didc}/bin/didc $out