Question about defining an update method with rust

When defining an update method with rust, what is the difference between

  1. #[update],
  2. #[update(guard = “is_user”)],
    and
  3. #[update(name = “funtionName”)] ?

#update
This marks the following fn as an update call and uses its name. If your next line is
fn my_awesome_update_func you could call your canister with this name and it would perform the update.

#[update(guard = “is_user”)]

The guard parameter can be read as this, in layman’s terms: Before you run my function, go run is_user, and only run my function if the guard returned OK().

Here’s a guard example function:

fn is_controller() -> Result<(), String> {
    RUNTIME_STATE.with(|state| {
        if state
            .borrow()
            .data
            .canister_settings
            .controllers
            .contains(&state.borrow().env.caller())
        {
            Ok(())
        } else {
            Err("You are not a controller".to_string())
        }
    })
}

And I use it like so:

#[update(name = "sendCycles", guard = "is_controller")]

async fn send_cycles() -> bool {
[...]

So my send_cycles fn gets “renamed” to sendCycles (as the js convention uses camelCase) and I also guard this function so it can be called only by the canister’s controllers.

I think I answered 2 & 3 with one example.

Also, while we’re talking about this, it’s worth mentioning the following macro:

#[candid_method(update, rename = "stop_cron")]

By annotating functions with this macro, we can then use the self rust → candid interface thingy, like so:

// Auto export the candid interface

candid::export_service!();

#[query(name = "__get_candid_interface_tmp_hack")]

#[candid_method(query, rename = "__get_candid_interface_tmp_hack")]

fn __export_did_tmp_() -> String {

__export_service()

}

This will take every function annotated with #[candid_method()] and add it to the candid output. Works like a charm for fast prototyping, and can be later modified to act as a test for your “manually curated” did file.

5 Likes

This is an awesome solution to my question!
Would it be possible to read the canister code where you define the static variable RUNTIME_STATE and contain more complete examples of how to use the “#[candid_method” macro?

Sure, I just pushed a “quickstart” project that I use to test things out. Check it out here

1 Like

@GLdev Hey, I cannot make this, guard = "is_controller")] you mentioned work, and I cannot find any use of it in your repo as well, could you give more details about it please ?

For that to work you’ll need to first get your canister’s controllers, have them in a vec or something and then iterate over that vec to check if the caller is on that list.

There are a few ways to get a canister’s controllers, but surprisingly not an ic_cdk simple API to do so. The easiest is to first add the return from “ic_cdk::api::caller()” inside your init() function. That will give you the dfx principal (with the latest dfx deploy). With that added you could have a manual function, guarded by the first controller, to add more controllers.

An advanced method is to call the management canister asking for a status of your own canister_id, and that will return ALL the controllers of a canister.

Check my post here. Also you can check my quickstart scaling repo for the implementation of the above is_controller. Note that how you add & keep track of controllers is up to you, until we get a simple ic_cdk api.