Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
6 changes: 6 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

== DFX

- feat: add --no-wallet flag and --wallet option to allow Users to bypass Wallet or specify a Wallet to use for calls (#1476)

Added `--no-wallet` flag to `dfx canister` and `dfx deploy`. This allows users to call canister management functionality with their Identity as the Sender (bypassing their Wallet canister.)
Added `--wallet` option to `dfx canister` and `dfx deploy`. This allows users to specify a wallet canister id to use as the Sender for calls.
`--wallet` and `--no-wallet` conflict with each other. Omitting both will invoke the selected Identitys wallet canister to perform calls.
Comment thread
p-shahi marked this conversation as resolved.
Outdated

- feat: dfx now provides CANISTER_ID_<canister_name> environment variables for all canisters to "npm build" when building the frontend.

== Agents
Expand Down
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")'
Comment thread
p-shahi marked this conversation as resolved.
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")'
}
25 changes: 24 additions & 1 deletion e2e/tests-dfx/wallet.bash
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ setup() {
mkdir "$x"
export HOME="$x"

dfx_new
dfx_new hello
}

teardown() {
Expand Down Expand Up @@ -45,3 +45,26 @@ teardown() {
assert_command dfx identity --network actuallylocal deploy-wallet "${ID_TWO}"
assert_match "The wallet canister \"${ID}\"\ already exists for user \"default\" on \"actuallylocal\" network."
}

@test "bypass wallet call as user" {
install_asset greet
dfx_start
assert_command dfx canister --no-wallet create --all
assert_command dfx build
assert_command dfx canister --no-wallet install hello
assert_command dfx canister --no-wallet call "$(dfx canister id hello)" greet '("DFINITY")'
assert_match '("Hello, DFINITY!")'
# Wallet should not have been created/should not exist
assert_command_fail test -f .dfx/local/wallets.json
}

@test "bypass wallet call as user: deploy" {
install_asset greet
dfx_start
assert_command dfx deploy --no-wallet
assert_command dfx canister --no-wallet call "$(dfx canister id hello)" greet '("DFINITY")'
assert_match '("Hello, DFINITY!")'
# Wallet should not have been created/should not exist
assert_command_fail test -f .dfx/local/wallets.json
}

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::lib::canister_info::CanisterInfo;
use crate::lib::environment::Environment;
use crate::lib::error::DfxResult;
use crate::lib::error::{DfxError, DfxResult};
use crate::lib::identity::identity_utils::{wallet_for_call_sender, CallSender};
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
Comment thread
p-shahi marked this conversation as resolved.
Outdated
{
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, false).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, false).await?;
// This is overkill, wallet.call should accept a Principal parameter
// Why do we need to construct a Canister?
Comment thread
p-shahi marked this conversation as resolved.
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, false).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
18 changes: 15 additions & 3 deletions src/dfx/src/commands/canister/create.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::lib::environment::Environment;
use crate::lib::error::DfxResult;
use crate::lib::identity::identity_utils::CallSender;
use crate::lib::operations::canister::create_canister;
use crate::lib::root_key::fetch_root_key_if_needed;
use crate::util::clap::validators::cycle_amount_validator;
Expand All @@ -26,19 +27,30 @@ pub struct CanisterCreateOpts {
with_cycles: Option<String>,
}

pub async fn exec(env: &dyn Environment, opts: CanisterCreateOpts) -> DfxResult {
pub async fn exec(
env: &dyn Environment,
opts: CanisterCreateOpts,
call_sender: &CallSender,
) -> DfxResult {
let config = env.get_config_or_anyhow()?;
let timeout = expiry_duration();

fetch_root_key_if_needed(env).await?;
let with_cycles = opts.with_cycles.as_deref();
if let Some(canister_name) = opts.canister_name.clone() {
create_canister(env, canister_name.as_str(), timeout, with_cycles).await
create_canister(
env,
canister_name.as_str(),
timeout,
with_cycles,
call_sender,
)
.await
} else if opts.all {
// Create all canisters.
if let Some(canisters) = &config.get_config().canisters {
for canister_name in canisters.keys() {
create_canister(env, canister_name, timeout, with_cycles).await?;
create_canister(env, canister_name, timeout, with_cycles, call_sender).await?;
}
}
Ok(())
Expand Down
Loading