diff --git a/e2e/tests-dfx/basic-project.bash b/e2e/tests-dfx/basic-project.bash index f23b670ed6..aede89d8b0 100644 --- a/e2e/tests-dfx/basic-project.bash +++ b/e2e/tests-dfx/basic-project.bash @@ -33,11 +33,19 @@ teardown() { assert_eq '("Hello, Bongalo!")' # Using call --async and request-status. - assert_command dfx canister call --async hello greet Blueberry + # Call with user Identity as Sender + assert_command dfx canister --no-wallet call --async hello greet Blueberry # At this point $output is the request ID. # shellcheck disable=SC2154 assert_command dfx canister request-status "$stdout" assert_eq '("Hello, Blueberry!")' + + # Call using the wallet's call forwarding + assert_command dfx canister call --async hello greet Blueberry + # At this point $output is the request ID. + # shellcheck disable=SC2154 + assert_command dfx canister request-status "$stdout" + assert_eq '(record { 153_986_224 = blob "DIDL\00\01q\11Hello, Blueberry!" })' } @test "build + install + call + request-status -- counter_mo" { @@ -79,9 +87,16 @@ teardown() { assert_eq "()" # Write has no return value. But we can _call_ read too. - assert_command dfx canister call hello read --async + # Call with user Identity as Sender + assert_command dfx canister --no-wallet call hello read --async assert_command dfx canister request-status "$stdout" assert_eq "(1_337)" + + # Call using the wallet's call forwarding + assert_command dfx canister call hello read --async + assert_command dfx canister request-status "$stdout" + assert_eq '(record { 153_986_224 = blob "DIDL\00\01}\b9\0a" })' + } @test "build + install + call -- counter_idl_mo" { diff --git a/e2e/tests-dfx/certificate.bash b/e2e/tests-dfx/certificate.bash index 5ffff42812..a89c3cded9 100644 --- a/e2e/tests-dfx/certificate.bash +++ b/e2e/tests-dfx/certificate.bash @@ -60,6 +60,9 @@ teardown() { } @test "mitm attack - query: attack succeeds because there is no certificate to verify" { - assert_command dfx canister call certificate hello_query '("Buckaroo")' + # The wallet does not have a query call forward method (currently calls forward from wallet's update method) + # So call with users Identity as sender here + # There may need to be a query version of wallet_call + assert_command dfx canister --no-wallet call certificate hello_query '("Buckaroo")' assert_eq '("Hullo, Buckaroo!")' } diff --git a/e2e/tests-dfx/identity.bash b/e2e/tests-dfx/identity.bash index 9e37910f9c..3e9381a60f 100644 --- a/e2e/tests-dfx/identity.bash +++ b/e2e/tests-dfx/identity.bash @@ -88,27 +88,32 @@ teardown() { assert_command dfx --identity alice build assert_command dfx --identity alice canister install --all + # The wallet is the initializer assert_command dfx --identity alice canister call e2e_project amInitializer + assert_eq '(true)' + + # The user Identity's principal is not the initializer + assert_command dfx --identity alice canister --no-wallet call e2e_project amInitializer assert_eq '(false)' - assert_command dfx --identity alice canister call \ + assert_command dfx --identity alice canister --no-wallet call \ "$(dfx --identity alice identity get-wallet)" wallet_call \ "(record { canister = principal \"$(dfx canister id e2e_project)\"; method_name = \"amInitializer\"; args = blob \"DIDL\00\00\"; cycles = (0:nat64)})" assert_eq '(record { 153_986_224 = blob "DIDL\00\01~\01" })' # True in DIDL. - assert_command dfx --identity bob canister call e2e_project amInitializer + assert_command dfx --identity bob canister --no-wallet call e2e_project amInitializer assert_eq '(false)' # these all fail (other identities are not initializer; cannot store assets): - assert_command_fail dfx --identity bob canister call e2e_project_assets store '("B", vec { 88; 87; 86 })' - assert_command_fail dfx --identity default canister call e2e_project_assets store '("B", vec { 88; 87; 86 })' - assert_command_fail dfx canister call e2e_project_assets store '("B", vec { 88; 87; 86 })' - assert_command_fail dfx canister call e2e_project_assets retrieve '("B")' + assert_command_fail dfx --identity bob canister --no-wallet call e2e_project_assets store '("B", vec { 88; 87; 86 })' + assert_command_fail dfx --identity default canister --no-wallet call e2e_project_assets store '("B", vec { 88; 87; 86 })' + assert_command_fail dfx canister --no-wallet call e2e_project_assets store '("B", vec { 88; 87; 86 })' + assert_command_fail dfx canister --no-wallet call e2e_project_assets retrieve '("B")' # but alice, the initializer, can store assets: assert_command dfx --identity alice canister call e2e_project_assets store '("B", vec { 88; 87; 86 })' assert_eq '()' - assert_command dfx canister call --output idl e2e_project_assets retrieve '("B")' + assert_command dfx canister --no-wallet call --output idl e2e_project_assets retrieve '("B")' assert_eq '(blob "XWV")' } @@ -120,24 +125,24 @@ teardown() { dfx --identity alice canister create --all assert_command dfx --identity alice build assert_command dfx --identity alice canister install --all - assert_command dfx --identity alice canister call \ + assert_command dfx --identity alice canister --no-wallet call \ "$(dfx --identity alice identity get-wallet)" wallet_call \ "(record { canister = principal \"$(dfx canister id e2e_project)\"; method_name = \"amInitializer\"; args = blob \"DIDL\00\00\"; cycles = (0:nat64)})" assert_eq '(record { 153_986_224 = blob "DIDL\00\01~\01" })' # True in DIDL. - assert_command dfx canister call e2e_project amInitializer + assert_command dfx canister --no-wallet call e2e_project amInitializer assert_eq '(false)' assert_command dfx identity rename alice bob assert_command dfx identity whoami assert_eq 'default' - assert_command dfx --identity bob canister call \ + assert_command dfx --identity bob canister --no-wallet call \ "$(dfx --identity bob identity get-wallet)" wallet_call \ "(record { canister = principal \"$(dfx canister id e2e_project)\"; method_name = \"amInitializer\"; args = blob \"DIDL\00\00\"; cycles = (0:nat64)})" assert_eq '(record { 153_986_224 = blob "DIDL\00\01~\01" })' # True in DIDL. assert_command dfx --identity bob canister call e2e_project_assets store '("B", blob "hello")' assert_eq '()' - assert_command dfx canister call --output idl e2e_project_assets retrieve '("B")' + assert_command dfx canister --no-wallet call --output idl e2e_project_assets retrieve '("B")' assert_eq '(blob "hello")' } diff --git a/src/dfx/src/commands/canister/call.rs b/src/dfx/src/commands/canister/call.rs index bd9b9aab38..31e4cd5dea 100644 --- a/src/dfx/src/commands/canister/call.rs +++ b/src/dfx/src/commands/canister/call.rs @@ -1,14 +1,20 @@ +use crate::commands::command_utils::{wallet_for_call_sender, CallSender}; use crate::lib::canister_info::CanisterInfo; use crate::lib::environment::Environment; -use crate::lib::error::DfxResult; +use crate::lib::error::{DfxError, DfxResult}; use crate::lib::models::canister_id_store::CanisterIdStore; use crate::lib::root_key::fetch_root_key_if_needed; use crate::lib::waiter::waiter_with_exponential_backoff; +use crate::util::clap::validators::cycle_amount_validator; use crate::util::{blob_from_arguments, expiry_duration, get_candid_type, print_idl_blob}; use anyhow::{anyhow, bail, Context}; +use candid::{CandidType, Deserialize}; use clap::Clap; use ic_types::principal::Principal as CanisterId; +use ic_utils::canister::{Argument, Canister}; +use ic_utils::interfaces::wallet::{CallForwarder, CallResult}; +use ic_utils::interfaces::Wallet; use std::option::Option; use std::path::PathBuf; @@ -50,6 +56,20 @@ pub struct CanisterCallOpts { #[clap(long, conflicts_with("async"), possible_values(&["idl", "raw", "pp"]))] output: Option, + + /// Specifies the amount of cycles to send on the call. + /// Deducted from the wallet. + #[clap(long, validator(cycle_amount_validator))] + with_cycles: Option, +} + +#[derive(CandidType, Deserialize)] +struct CallIn { + canister: CanisterId, + method_name: String, + #[serde(with = "serde_bytes")] + args: Vec, + cycles: u64, } fn get_local_cid_and_candid_path( @@ -65,7 +85,38 @@ fn get_local_cid_and_candid_path( )) } -pub async fn exec(env: &dyn Environment, opts: CanisterCallOpts) -> DfxResult { +async fn do_wallet_call(wallet: &Canister<'_, Wallet>, args: &CallIn) -> DfxResult> { + let (result,): (CallResult,) = wallet + .update_("wallet_call") + .with_arg(args) + .build() + .call_and_wait(waiter_with_exponential_backoff()) + .await?; + Ok(result.r#return) +} + +async fn request_id_via_wallet_call( + wallet: &Canister<'_, Wallet>, + canister: &Canister<'_>, + method_name: &str, + args: Argument, + cycles: u64, +) -> DfxResult +where +{ + let call_forwarder: CallForwarder<'_, '_, (CallResult,)> = + wallet.call(canister, method_name, args, cycles); + call_forwarder + .call() + .await + .map_err(|err| anyhow!("Agent error {}", err)) +} + +pub async fn exec( + env: &dyn Environment, + opts: CanisterCallOpts, + call_sender: &CallSender, +) -> DfxResult { let callee_canister = opts.canister_name.as_str(); let method_name = opts.method_name.as_str(); let canister_id_store = CanisterIdStore::for_env(env)?; @@ -124,29 +175,87 @@ pub async fn exec(env: &dyn Environment, opts: CanisterCallOpts) -> DfxResult { let timeout = expiry_duration(); + // amount has been validated by cycle_amount_validator + let cycles = opts + .with_cycles + .as_deref() + .map_or(0_u64, |amount| amount.parse::().unwrap()); + if is_query { - let blob = agent - .query(&canister_id, method_name) - .with_arg(&arg_value) - .call() - .await?; + let blob = match call_sender { + CallSender::SelectedId => { + agent + .query(&canister_id, method_name) + .with_arg(&arg_value) + .call() + .await? + } + CallSender::Wallet(some_id) | CallSender::SelectedIdWallet(some_id) => { + let wallet = wallet_for_call_sender(env, call_sender, some_id).await?; + do_wallet_call( + &wallet, + &CallIn { + canister: canister_id, + method_name: method_name.to_string(), + args: arg_value, + cycles, + }, + ) + .await? + } + }; print_idl_blob(&blob, output_type, &method_type) .context("Invalid data: Invalid IDL blob.")?; } else if opts.r#async { - let request_id = agent - .update(&canister_id, method_name) - .with_arg(&arg_value) - .call() - .await?; + let request_id = match call_sender { + CallSender::SelectedId => { + agent + .update(&canister_id, method_name) + .with_arg(&arg_value) + .call() + .await? + } + CallSender::Wallet(some_id) | CallSender::SelectedIdWallet(some_id) => { + let wallet = wallet_for_call_sender(env, call_sender, some_id).await?; + // This is overkill, wallet.call should accept a Principal parameter + // Why do we need to construct a Canister? + let canister = Canister::builder() + .with_agent(agent) + .with_canister_id(canister_id) + .build() + .map_err(DfxError::from)?; + let mut args = Argument::default(); + args.set_raw_arg(arg_value); + + request_id_via_wallet_call(&wallet, &canister, method_name, args, cycles).await? + } + }; eprint!("Request ID: "); println!("0x{}", String::from(request_id)); } else { - let blob = agent - .update(&canister_id, method_name) - .with_arg(&arg_value) - .expire_after(timeout) - .call_and_wait(waiter_with_exponential_backoff()) - .await?; + let blob = match call_sender { + CallSender::SelectedId => { + agent + .update(&canister_id, method_name) + .with_arg(&arg_value) + .expire_after(timeout) + .call_and_wait(waiter_with_exponential_backoff()) + .await? + } + CallSender::Wallet(some_id) | CallSender::SelectedIdWallet(some_id) => { + let wallet = wallet_for_call_sender(env, call_sender, some_id).await?; + do_wallet_call( + &wallet, + &CallIn { + canister: canister_id, + method_name: method_name.to_string(), + args: arg_value, + cycles, + }, + ) + .await? + } + }; print_idl_blob(&blob, output_type, &method_type) .context("Invalid data: Invalid IDL blob.")?; diff --git a/src/dfx/src/commands/canister/mod.rs b/src/dfx/src/commands/canister/mod.rs index ab1162b553..a5b02b6846 100644 --- a/src/dfx/src/commands/canister/mod.rs +++ b/src/dfx/src/commands/canister/mod.rs @@ -63,7 +63,7 @@ pub fn exec(env: &dyn Environment, opts: CanisterOpts) -> DfxResult { runtime.block_on(async { let call_sender = call_sender(&agent_env, &opts.wallet, opts.no_wallet).await?; match opts.subcmd { - SubCommand::Call(v) => call::exec(&agent_env, v).await, + SubCommand::Call(v) => call::exec(&agent_env, v, &call_sender).await, SubCommand::Create(v) => create::exec(&agent_env, v, &call_sender).await, SubCommand::Delete(v) => delete::exec(&agent_env, v, &call_sender).await, SubCommand::Id(v) => id::exec(&agent_env, v).await, diff --git a/src/dfx/src/commands/command_utils.rs b/src/dfx/src/commands/command_utils.rs index 993dfa487d..3fd0df73c6 100644 --- a/src/dfx/src/commands/command_utils.rs +++ b/src/dfx/src/commands/command_utils.rs @@ -2,7 +2,10 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::identity::Identity; +use anyhow::anyhow; use ic_types::principal::Principal; +use ic_utils::canister::Canister; +use ic_utils::interfaces::Wallet; #[derive(Debug, PartialEq)] pub enum CallSender { @@ -38,3 +41,28 @@ pub async fn call_sender( }; Ok(sender) } + +#[allow(clippy::needless_lifetimes)] +pub async fn wallet_for_call_sender<'env>( + env: &'env dyn Environment, + call_sender: &CallSender, + some_id: &Option, +) -> DfxResult> { + let network = env + .get_network_descriptor() + .expect("No network descriptor."); + let identity_name = env.get_selected_identity().expect("No selected identity."); + if call_sender == &CallSender::Wallet(some_id.clone()) { + let id = some_id + .as_ref() + .expect("Wallet canister id should have been provided here."); + Identity::build_wallet_canister(id.clone(), env) + } else if call_sender == &CallSender::SelectedIdWallet(some_id.clone()) { + Identity::get_wallet_canister(env, network, &identity_name).await + } else { + Err(anyhow!( + "Attempted get a wallet for an invalid CallSender variant: {:?}", + call_sender + )) + } +}