Home

Awesome

Canister REPL

ic-repl [--replica [local|ic|url] | --offline [--format [json|ascii|png]]] --config <toml config> [script file] --verbose

Commands

<command> := 
 | import <id> = <text> (as <text>)?                // bind canister URI to <id>, with optional did file
 | load <exp>                                       // load and run a script file. Do not error out if <exp> ends with '?'
 | config <text>                                    // set config in TOML format
 | let <id> = <exp>                                 // bind <exp> to a variable <id>
 | <exp>                                            // show the value of <exp>
 | assert <exp> <binop> <exp>                       // assertion
 | identity <id> (<text> | record { slot_index = <nat>; key_id = <text> })?   // switch to identity <id>, with optional pem file or HSM config
 | function <id> ( <id>,* ) { <command>;* }         // define a function
 | if <exp> { <command>;* } else { <command>;* }    // conditional branch
 | while <exp> { <command>;* }                      // while loop
<exp> := 
 | <candid val>                                     // any candid value
 | <var> <transformer>*                             // variable with optional transformers
 | fail <exp>                                       // convert error message as text
 | call (as <name>)? <name> . <name> (( <exp>,* ))? // call a canister method, and store the result as a single value
 | par_call [ (<name> . <name> (( <exp>,* ))),* ]   // make concurrent canister calls, and store the result as a tuple record
 | encode (<name> . <name>)? (( <exp>,* ))?         // encode candid arguments as a blob value. canister.__init_args represents init args
 | decode (as <name> . <name>)? <exp>               // decode blob as candid values
 | <id> ( <exp>,* )                                 // function application
<var> := 
 | <id>                  // variable name 
 | _                     // previous eval of exp is bind to `_`
<transformer> :=
 | ?                     // select opt value
 | . <name>              // select field name from record or variant value
 | [ <exp> ]             // select index from vec, text, record, or variant value
 | . <id> ( <exp>,* )    // transform (map, filter, fold) a collection value
<binop> := 
 | ==                    // structural equality
 | ~=                    // equal under candid subtyping; for text value, we check if the right side is contained in the left side
 | !=                    // not equal

Functions

Similar to most shell languages, functions in ic-repl is dynamically scoped and untyped.

We also provide some built-in functions:

The following functions are only available in non-offline mode:

There is a special __main function you can define in the script, which gets executed when loading from CLI. __main can take arguments provided from CLI. The CLI arguments gets parsed by the Candid value parser first. If parsing fails, it is stored as a text value. For example, the following code can be called with ic-repl main.sh -- test 42 and outputs "test43".

main.sh

function __main(name, n) {
  stringify(name, add(n, 1))
}

Object methods

For vec, record or text value, we provide some built-in methods for value transformation:

For record value, v[i] is represented as record { key; value } sorted by field id.

For text value, v[i] is represented as a text value containing a single character.

Type casting

Type annotations in ic-repl is more permissible (not following the subtyping rules) than the Candid library to allow piping results from different canister calls.

Examples

test.sh

#!/usr/bin/ic-repl -r ic
// assume we already installed the greet canister
import greet = "rrkah-fqaaa-aaaaa-aaaaq-cai";
call greet.greet("test");
let result = _;
assert _ == "Hello, test!";
identity alice;
call "rrkah-fqaaa-aaaaa-aaaaq-cai".greet("test");
assert _ == result;

nns.sh

#!/usr/bin/ic-repl -r ic
// nns and ledger canisters are auto-imported if connected to the mainnet
call nns.get_pending_proposals()
identity private "./private.pem";
call ledger.account_balance(record { account = account(private) });

function transfer(to, amount, memo) {
  call ledger.transfer(
    record {
      to = to;
      fee = record { e8s = 10_000 };
      memo = memo;
      from_subaccount = null;
      created_at_time = null;
      amount = record { e8s = amount };
    },
  );
};
function stake(amount, memo) {
  let _ = transfer(neuron_account(private, memo), amount, memo);
  call nns.claim_or_refresh_neuron_from_account(
    record { controller = opt private; memo = memo }
  );
  _.result?.NeuronId
};
let neuron_id = stake(100_000_000, 42);

install.sh

#!/usr/bin/ic-repl
function deploy(wasm) {
  let id = call ic.provisional_create_canister_with_cycles(record { settings = null; amount = null });
  call ic.install_code(
    record {
      arg = encode wasm.__init_args();
      wasm_module = wasm;
      mode = variant { install };
      canister_id = id.canister_id;
    },
  );
  id
};

identity alice;
let id = deploy(file("greet.wasm"));
let canister = id.canister_id;
let res = par_call [ic.canister_status(id), canister.greet("test")];
let status = res[0];
assert status.settings ~= record { controllers = vec { alice } };
assert status.module_hash? == blob "...";
assert res[1] == "Hello, test!";

wallet.sh

#!/usr/bin/ic-repl
import wallet = "${WALLET_ID:-rwlgt-iiaaa-aaaaa-aaaaa-cai}" as "wallet.did";
identity default "~/.config/dfx/identity/default/identity.pem";
call wallet.wallet_create_canister(
  record {
    cycles = ${CYCLE:-1_000_000};
    settings = record {
      controllers = null;
      freezing_threshold = null;
      memory_allocation = null;
      compute_allocation = null;
    };
  },
);
let id = _.Ok.canister_id;
call as wallet ic.install_code(
  record {
    arg = encode ();
    wasm_module = file("${WASM_FILE}");
    mode = variant { install };
    canister_id = id;
  },
);
call id.greet("test");

profiling.sh

#!/usr/bin/ic-repl
import "install.sh";

let file = "result.md";
output(file, "# profiling result\n\n");
output(file, "|generate|get|put|\n|--:|--:|--:|\n");

let cid = deploy(gzip(wasm_profiling("hashmap.wasm")));
call cid.__toggle_tracing();   // Disable flamegraph tracing
call cid.generate(50000);
output(file, stringify(__cost__, "|"));

call cid.__toggle_tracing();   // Enable flamegraph tracing
call cid.batch_get(50);
flamegraph(cid, "hashmap.get(50)", "get");
output(file, stringify("[", __cost__, "](get.svg)|"));

let put = call cid.batch_put(50);
flamegraph(cid, "hashmap.put(50)", "put.svg");
output(file, stringify("[", __cost_put, "](put.svg)|\n"));

recursion.sh

function fib(n) {
  let _ = ite(lt(n, 2), 1, add(fib(sub(n, 1)), fib(sub(n, 2))))
};
function fib2(n) {
  let a = 1;
  let b = 1;
  while gt(n, 0) {
      let b = add(a, b);
      let a = sub(b, a);
      let n = sub(n, 1);
  };
  let _ = a;
};
function fib3(n) {
  if lt(n, 2) {
      let _ = 1;
  } else {
      let _ = add(fib3(sub(n, 1)), fib3(sub(n, 2)));
  }
};
assert fib(10) == 89;
assert fib2(10) == 89;
assert fib3(10) == 89;

Relative paths

Several commands and functions are taking arguments from the file system. We have different definitions for relative paths, depending on whether you are reading or writing the file.

The rationale for the difference is that we can have an easier time to control where the output files are located, as scripts can spread out in different directories.

Derived forms

let _ = call proxy_canister.wallet_call(
  record {
    args = encode target_canister.method(args);
    cycles = 0;
    method_name = "method";
    canister = principal "target_canister";
  }
);
decode as target_canister.method _.Ok.return

Canister init args types

When calling ic.install_code, you may need to provide a Candid message for initializing the canister. To help with encoding the message, you can use get the init args types from the Wasm module custom section:

let wasm = file("a.wasm");
encode wasm.__init_args(...)

If the Wasm module doesn't contain the init arg types, you can import the full did file as a workaround:

import init = "2vxsx-fae" as "did_file_with_init_args.did";
encode init.__init_args(...)

Contributing

Please follow the guidelines in the CONTRIBUTING.md document.

Issues