Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions e2e/tests-dfx/basic-project.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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" {
Expand Down
5 changes: 4 additions & 1 deletion e2e/tests-dfx/certificate.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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!")'
}
27 changes: 16 additions & 11 deletions e2e/tests-dfx/identity.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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")'
}

Expand All @@ -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")'
}
145 changes: 127 additions & 18 deletions src/dfx/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -50,6 +56,20 @@ pub struct CanisterCallOpts {
#[clap(long, conflicts_with("async"),
possible_values(&["idl", "raw", "pp"]))]
output: Option<String>,

/// Specifies the amount of cycles to send on the call.
/// Deducted from the wallet.
#[clap(long, validator(cycle_amount_validator))]
with_cycles: Option<String>,
}

#[derive(CandidType, Deserialize)]
struct CallIn {
canister: CanisterId,
method_name: String,
#[serde(with = "serde_bytes")]
args: Vec<u8>,
cycles: u64,
}

fn get_local_cid_and_candid_path(
Expand All @@ -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<Vec<u8>> {
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<ic_agent::RequestId>
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)?;
Expand Down Expand Up @@ -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::<u64>().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.")?;
Expand Down
2 changes: 1 addition & 1 deletion src/dfx/src/commands/canister/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions src/dfx/src/commands/command_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Principal>,
) -> DfxResult<Canister<'env, Wallet>> {
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
))
}
}