IC Management Canister

The Internet Computer (IC) provides a management canister to manage canisters programmatically. This canister, like any other canister, has a Candid Interface and can be called by other canisters or ingress messages.

In this chapter, we will only look at a subset of the interface for canister management.

Motoko Interface

This is a subset of the interface as a Motoko module. It only includes canister management related types and functions. It is available as ic-management-interface.mo

Types

  • canister_id
  • canister_settings
  • definite_canister_settings
  • wasm_module
  • Self

Public functions

module {
  public type canister_id = Principal;

  public type canister_settings = {
    freezing_threshold : ?Nat;
    controllers : ?[Principal];
    memory_allocation : ?Nat;
    compute_allocation : ?Nat;
  };

  public type definite_canister_settings = {
    freezing_threshold : Nat;
    controllers : [Principal];
    memory_allocation : Nat;
    compute_allocation : Nat;
  };

  public type wasm_module = [Nat8];

  public type Self = actor {
    canister_status : shared { canister_id : canister_id } -> async {
      status : { #stopped; #stopping; #running };
      memory_size : Nat;
      cycles : Nat;
      settings : definite_canister_settings;
      idle_cycles_burned_per_day : Nat;
      module_hash : ?[Nat8];
    };

    create_canister : shared { settings : ?canister_settings } -> async {
      canister_id : canister_id;
    };

    delete_canister : shared { canister_id : canister_id } -> async ();

    deposit_cycles : shared { canister_id : canister_id } -> async ();

    install_code : shared {
      arg : [Nat8];
      wasm_module : wasm_module;
      mode : { #reinstall; #upgrade; #install };
      canister_id : canister_id;
    } -> async ();

    start_canister : shared { canister_id : canister_id } -> async ();

    stop_canister : shared { canister_id : canister_id } -> async ();

    uninstall_code : shared { canister_id : canister_id } -> async ();

    update_settings : shared {
      canister_id : Principal;
      settings : canister_settings;
    } -> async ();

  };
};

Import

We import the management canister by importing the interface file and declaring an actor by principle aaaaa-aa and type it as the Self (which is declared in the interface).

import Interface "ic-management-interface";
import Cycles "mo:base/ExperimentalCycles";

actor {
    // The IC Management Canister ID
    let IC = "aaaaa-aa";
    let ic = actor(IC) : Interface.Self;
}

We can now reference the canister as ic.

We also imported ExperimentalCycles because some of our function calls require cycles to be added.

Public functions

The source file with function calls is available here including a test to run all functions. You can deploy it locally for testing.

Create canister

To create a new canister, we call the create_canister function.

create_canister : shared { settings : ?canister_settings } -> async {
    canister_id : canister_id;
};

The function may take an optional canisters_settings record to set initial settings for the canister, but this argument may be null.

The function returns a record containing the Principal of the newly created canister.

NOTE
To create a new canister, you must add cycles to the call using the ExperimentalCycles module

Example

    var canister_principal : Text = "";

    func create_canister() : async* () {
        Cycles.add(10 ** 12);

        let newCanister = await ic.create_canister({ settings = null });

        canister_principal := Principal.toText(newCanister.canister_id);
    };

Canister status

To get the current status of a canister we call canister_status. We only provide a simple record with a canister_id (principal) of the canister we are interested in. Only controllers of the canister can ask for its settings.

canister_status : shared { canister_id : canister_id } -> async {
    status : { #stopped; #stopping; #running };
    memory_size : Nat;
    cycles : Nat;
    settings : definite_canister_settings;
    idle_cycles_burned_per_day : Nat;
    module_hash : ?[Nat8];
};

The function returns a record containing the status of the canister, the memory_size in bytes, the cycles balance, a definite_canister_settings with its current settings, the idle_cycles_burned_per_day which indicates the average cycle consumption of the canister and a module_hash if the canister has a wasm module installed on it.

Example

    var controllers : [Principal] = [];

    func canister_status() : async* () {
        let canister_id = Principal.fromText(canister_principal);

        let canisterStatus = await ic.canister_status({ canister_id });

        controllers := canisterStatus.settings.controllers;
    };

Deposit cycles

To deposit cycles into a canister we call deposit_cycles. Anyone can call this function.

We only need to provide a record with the canister_id of the canister we want to deposit into.

deposit_cycles : shared { canister_id : canister_id } -> async ();

NOTE
To deposit cycles into a canister, you must add cycles to the call using the ExperimentalCycles module

Example

    func deposit_cycles() : async* () {
        Cycles.add(10 ** 12);

        let canister_id = Principal.fromText(canister_principal);

        await ic.deposit_cycles({ canister_id });
    };

Update settings

To update the settings of a canister, we call update_settings and provide the canister_id together with the new canister_settings.

update_settings : shared {
      canister_id : Principal;
      settings : canister_settings;
} -> async ();

Example

    func update_settings() : async* () {
        let settings : Interface.canister_settings = {
            controllers = ?controllers;
            compute_allocation = null;
            memory_allocation = null;
            freezing_threshold = ?(60 * 60 * 24 * 7);
        };

        let canister_id = Principal.fromText(canister_principal);

        await ic.update_settings({ canister_id; settings });
    };

Uninstall code

To uninstall (remove) the wasm module from a canister we call uninstall_code with a record containing the canister_id. Only controllers of the canister can call this function.

uninstall_code : shared { canister_id : canister_id } -> async ();

Example

    func uninstall_code() : async* () {
        let canister_id = Principal.fromText(canister_principal);

        await ic.uninstall_code({ canister_id });
    };

Stop canister

To stop a running canister we call stop_canister with a record containing the canister_id. Only controllers of the canister can call this function.

stop_canister : shared { canister_id : canister_id } -> async ();

Example

    func stop_canister() : async* () {
        let canister_id = Principal.fromText(canister_principal);

        await ic.stop_canister({ canister_id });
    };

Start canister

To start a stopped canister we call start_canister with a record containing the canister_id. Only controllers of the canister can call this function.

start_canister : shared { canister_id : canister_id } -> async ();

Example

    func start_canister() : async* () {
        let canister_id = Principal.fromText(canister_principal);

        await ic.start_canister({ canister_id });
    };

Delete canister

To delete a stopped canister we call delete_canister with a record containing the canister_id. Only stopped canisters can be deleted and only controllers of the canister can call this function.

delete_canister : shared { canister_id : canister_id } -> async ();

Example

    func delete_canister() : async* () {
        let canister_id = Principal.fromText(canister_principal);

        await ic.delete_canister({ canister_id });
    };

Install code

To install a wasm module in a canister, we call install_code. Only controllers of a canister can call this function.

We need to provide a wasm module install arguments as [Nat8] arrays. We also pick a mode to indicate whether we are freshly installing or upgrading the canister. And finally, we provide the canister id (principal) that we want to install code into.

install_code : shared {
    arg : [Nat8];
    wasm_module : wasm_module;
    mode : { #reinstall; #upgrade; #install };
    canister_id : canister_id;
} -> async ();

This function is atomic meaning that it either succeeds and returns () or it has no effect.

Test

To test all the functions, we await* all of them in a try-catch block inside a regular shared public function. This test is available in ic-management-public-functions.mo.

    public func ic_management_canister_test() : async { #OK; #ERR : Text } {
        try {
            await* create_canister();
            await* canister_status();

            await* deposit_cycles();
            await* update_settings();
            await* uninstall_code();

            await* stop_canister();
            await* start_canister();
            await* stop_canister();

            await* delete_canister();

            #OK;
        } catch (e) {
            #ERR(Error.message(e));
        };
    };

Our function either returns #OK or #ERR with a caught error message that is converted into text.