sasa-tomic

icp-backend-motoko

0
0
# Install this skill:
npx skills add sasa-tomic/icp-skills --skill "icp-backend-motoko"

Install specific skill from multi-skill repository

# Description

Develop ICP canister backends using Motoko. Covers actors, types, async patterns, stable memory, upgrades, access control, and error handling. Use when writing Motoko code, creating canisters, working with actors, or asking about Motoko syntax and patterns.

# SKILL.md


name: icp-backend-motoko
description: Develop ICP canister backends using Motoko. Covers actors, types, async patterns, stable memory, upgrades, access control, and error handling. Use when writing Motoko code, creating canisters, working with actors, or asking about Motoko syntax and patterns.


ICP Backend Development with Motoko

Motoko is a programming language designed specifically for the Internet Computer Protocol (ICP). It makes writing canister smart contracts intuitive with built-in support for actors, async messaging, and stable memory.

Quick Reference

Project Structure

my_canister/
β”œβ”€β”€ dfx.json              # Project config
└── src/
    └── my_canister/
        └── main.mo       # Main actor file

Minimal Actor

actor {
  public func greet(name : Text) : async Text {
    "Hello, " # name # "!"
  };
}

Create & Deploy

dfx new my_project --type motoko --no-frontend
cd my_project
dfx start --background
dfx deploy
dfx canister call my_project_backend greet '("World")'

Core Concepts

Actors

Every Motoko canister is an actor - an isolated unit with private state that communicates via async messages.

actor Counter {
  var count : Nat = 0;  // Private mutable state

  public func inc() : async Nat {
    count += 1;
    count
  };

  public query func get() : async Nat {
    count
  };
}

Key points:
- actor { } defines a canister
- State (var) is private by default
- public functions are callable externally
- query functions are fast reads (no state changes)
- All public functions return async T

Variables and Declarations

let x : Nat = 42;        // Immutable
var y : Nat = 0;         // Mutable
y := y + 1;              // Assignment uses :=

// Block expressions with do { }
let result = do {
  let a = 1;
  let b = 2;
  a + b  // Last expression is the result
};

// Discard return values with ignore
ignore await someCanister.fire();

Types

Type Description Example
Nat Natural numbers (0, 1, 2, ...) let n : Nat = 42
Int Integers (..., -1, 0, 1, ...) let i : Int = -5
Text Unicode strings "hello"
Bool Boolean true, false
Blob Binary data "\00\01\02"
Principal Identity/canister ID Principal.fromText("...")
?T Optional (null or value) ?Nat, null, ?42
[T] Immutable array [1, 2, 3]
[var T] Mutable array [var 1, 2, 3]
(T1, T2) Tuple (42, "hello")
{a: T1; b: T2} Record/object {name = "Bob"; age = 30}
#tag1 \| #tag2 Variant #ok(42), #err("fail")

Imports

import Debug "mo:base/Debug";       // Base library
import Array "mo:base/Array";
import Principal "mo:base/Principal";
import Time "mo:base/Time";
import Result "mo:base/Result";
import Types "./types";             // Local module (relative path)

Common base library modules: Array, Blob, Buffer, Debug, Error, Float, Hash, HashMap, Int, Iter, List, Nat, Option, OrderedMap, OrderedSet, Principal, Random, Region, Result, Text, Time, Timer, TrieMap.

Note: A new mo:core standard library is being developed with improved data structures that are natively stable (no pre/post-upgrade hooks needed). For new projects, consider using mo:core/Map, mo:core/Set instead of HashMap/TrieMap. See the base-to-core migration guide for details.

Package Management with mops

mops is the Motoko package manager.

# Install mops (requires Node.js)
npm i -g ic-mops

# Initialize in project
mops init

# Add a package
mops add base          # Base library
mops add map           # Third-party package
mops add github-user/repo  # GitHub package

# Install dependencies
mops install

# Update packages
mops update

mops.toml configuration:

[package]
name = "my_canister"
version = "0.1.0"

[dependencies]
base = "0.11.1"
map = "9.0.1"

Using packages:

import Map "mo:map/Map";           // From mops package
import { phash } "mo:map/Map";     // Named import
import Base "mo:base/Array";       // Base library (also via mops)

dfx.json configuration for mops:

{
  "defaults": {
    "build": {
      "packtool": "mops sources"
    }
  }
}

Pipe Operator

The pipe operator |> makes nested function calls more readable by flowing data left-to-right:

import Iter "mo:base/Iter";
import Array "mo:base/Array";
import List "mo:base/List";

// Without pipes - hard to read
let result1 = { data = Array.filter(List.toArray(List.fromArray(Iter.toArray(Iter.range(0, 10)))), func(n : Nat) : Bool { n % 3 == 0 }) };

// With pipes - reads naturally
let result2 = Iter.range(0, 10)
  |> Iter.toArray(_)
  |> List.fromArray(_)
  |> List.toArray(_)
  |> Array.filter(_, func(n : Nat) : Bool { n % 3 == 0 })
  |> { data = _ };

Use _ as the placeholder for the piped value. Multiple _ references in the same pipe step refer to the same value.


Async and Inter-Canister Calls

Shared Functions

Shared functions are callable remotely and return futures:

public shared func process(data : Text) : async Text {
  // Can call other canisters here
  "processed: " # data
}

Await

Use await to get the result of an async call:

let result : Text = await someCanister.process("input");

Caller Identification

shared(msg) actor class MyActor() {
  let owner = msg.caller;  // Principal who deployed

  public shared(msg) func whoami() : async Principal {
    msg.caller  // Principal of current caller
  };
}

Computation Types (async/await)

Use async* for efficient async abstraction without the overhead of full futures:

actor {
  var logging = true;

  // async* - no message overhead, runs inline
  func maybeLog(msg : Text) : async* () {
    if (logging) { await Logger.log(msg) };
  };

  public func doStuff() : async () {
    await* maybeLog("step 1");  // Runs inline, no extra message
    await* maybeLog("step 2");
  };
}
Feature async / await async* / await*
Message cost Sends message to self None (inline execution)
Execution Eager - schedules immediately Lazy - runs when awaited
Repeatable No - same future, same result Yes - each await* re-runs
Commit point Yes - state committed at await No - not a commit point

Warning: await* is NOT a commit point. Traps may roll back to the last await, not to the await*.


Randomness

ICP provides cryptographic randomness via the management canister's VRF:

import Random "mo:base/Random";

actor {
  // Get 32 bytes of cryptographic randomness
  public func getRandomBytes() : async Blob {
    await Random.blob()
  };

  // Use Random.Finite for structured random values
  public func flipCoin() : async ?Bool {
    let entropy = await Random.blob();
    let random = Random.Finite(entropy);
    random.coin()  // ?true or ?false
  };

  // Generate random number in range [0, 2^p - 1]
  public func randomInRange(bits : Nat8) : async ?Nat {
    let random = Random.Finite(await Random.blob());
    random.range(bits)
  };
}
Method Security Use Case
Random.blob() Cryptographic Secure keys, fairness-critical
Random.Finite Cryptographic Structured random values
fuzz package Seed-dependent Testing, procedural generation
idempotency-keys Seed-dependent UUID v4 generation

Persistence and Upgrades

Motoko provides orthogonal persistence - your program state automatically persists across calls without explicit database operations.

Enhanced Orthogonal Persistence (Default)

Enhanced orthogonal persistence (EOP) is the default mode. It uses a stable heap that persists main memory across upgrades with 64-bit addressing:

// Use 'persistent actor' for EOP (recommended)
persistent actor Counter {
  var count = 0;  // Automatically persists across upgrades!

  public func inc() : async Nat {
    count += 1;
    count
  };
}
Feature Enhanced (EOP) Classical
Heap limit 64-bit (large) 4GB (32-bit)
Upgrade speed Fast (no serialization) Slow (Candid serialization)
Stable keyword Optional (all vars stable) Required for persistence
Default since moc 0.13+ Legacy

Compatible upgrade changes (EOP automatically handles):
- Adding/removing actor fields
- Changing let to var and vice-versa
- Removing object fields, adding variant fields
- Changing Nat to Int

Classical Persistence (Legacy)

Use stable keyword to persist specific variables (legacy mode):

actor {
  stable var counter : Nat = 0;  // Survives upgrades
  var cache : Text = "";          // Lost on upgrade
}

Enable legacy mode with compiler flag --legacy-persistence.

Stable Variables

Use stable to persist data across upgrades:

actor {
  stable var counter : Nat = 0;  // Survives upgrades
  var cache : Text = "";          // Lost on upgrade

  public func inc() : async Nat {
    counter += 1;
    counter
  };
}

Transient Variables

In a persistent actor, all fields are stable by default. Use transient for fields that should reset on upgrade:

persistent actor {
  var count = 0;                      // Implicitly stable, survives upgrades

  transient var cache : [Text] = [];  // Reset to [] on every upgrade
  transient let rng = Random.crypto(); // Objects with methods must be transient
}

When to use transient:
- Caches that can be rebuilt
- Iterators (can't be serialized)
- Objects with methods (not stable types)
- Session state that shouldn't persist

Pre/Post Upgrade Hooks

For complex data structures that aren't directly stable:

import Array "mo:base/Array";

actor {
  stable var stableEntries : [(Text, Nat)] = [];
  var map = HashMap.HashMap<Text, Nat>(10, Text.equal, Text.hash);

  system func preupgrade() {
    stableEntries := Iter.toArray(map.entries());
  };

  system func postupgrade() {
    map := HashMap.fromIter(stableEntries.vals(), 10, Text.equal, Text.hash);
    stableEntries := [];
  };
}

Upgrade Command

dfx canister install my_canister --mode upgrade

Access Control

Basic Principal Check

import Principal "mo:base/Principal";

shared(msg) actor class SecureActor() {
  let owner = msg.caller;

  public shared(msg) func adminOnly() : async () {
    assert(msg.caller == owner);
    // admin action
  };

  public shared(msg) func rejectAnonymous() : async () {
    assert(not Principal.isAnonymous(msg.caller));
  };
}

Role-Based Access

public type Role = { #owner; #admin; #user };

stable var roles : [(Principal, Role)] = [];

func getRole(p : Principal) : ?Role {
  for ((principal, role) in roles.vals()) {
    if (principal == p) return ?role;
  };
  null
};

func requireRole(caller : Principal, required : Role) {
  switch (getRole(caller)) {
    case (?#owner) {};  // Owner can do anything
    case (?role) { assert(role == required) };
    case null { assert(false) };
  };
};

Error Handling

Option Types

func find(id : Nat) : ?Text {
  if (id == 1) { ?"found" } else { null }
};

switch (find(1)) {
  case null { Debug.print("not found") };
  case (?value) { Debug.print(value) };
};

Result Types

import Result "mo:base/Result";

type MyError = { #notFound; #unauthorized; #invalid : Text };

func process(id : Nat) : Result.Result<Text, MyError> {
  if (id == 0) { #err(#invalid("id cannot be 0")) }
  else if (id > 100) { #err(#notFound) }
  else { #ok("success") }
};

Throwing Errors (Async Context Only)

import Error "mo:base/Error";

public func riskyOperation() : async () {
  throw Error.reject("Something went wrong");
};

public func safeCall() : async Text {
  try {
    await riskyOperation();
    "success"
  } catch (e) {
    "failed: " # Error.message(e)
  }
};

Try-Finally for Cleanup

Use finally to run cleanup code regardless of success or failure:

public func withCleanup() : async () {
  var resource = acquireResource();
  try {
    await processResource(resource);
  } finally {
    releaseResource(resource);  // Always runs
  }
};

Best Practice: Prefer Result over exceptions for expected errors. Use exceptions for truly exceptional conditions.


Common Patterns

Actor Classes (Multiple Instances)

// Bucket.mo
actor class Bucket(n : Nat, i : Nat) {
  var data : [var ?Text] = Array.init(n, null);

  public func put(key : Nat, value : Text) : async () {
    assert((key % n) == i);
    data[key] := ?value;
  };
};

Timers

import Timer "mo:base/Timer";

actor {
  stable var count = 0;

  let timerId = Timer.recurringTimer<system>(#seconds 60, func() : async () {
    count += 1;
    Debug.print("Tick: " # Nat.toText(count));
  });

  public func cancelTimer() : async () {
    Timer.cancelTimer(timerId);
  };
}

Message Inspection (DoS Protection)

import Principal "mo:base/Principal";

actor {
  system func inspect({ caller : Principal; arg : Blob }) : Bool {
    // Reject anonymous callers
    if (Principal.isAnonymous(caller)) return false;
    // Reject large payloads
    if (arg.size() > 1024) return false;
    true
  };
}

Optimization

Enable wasm-opt in dfx.json

{
  "canisters": {
    "my_canister": {
      "type": "motoko",
      "main": "src/my_canister/main.mo",
      "optimize": "cycles"
    }
  }
}

Options:
- "cycles" - Optimize for cycle usage (~10% reduction)
- "size" - Optimize for binary size (~16% reduction)

Incremental Garbage Collection

{
  "canisters": {
    "my_canister": {
      "type": "motoko",
      "main": "src/my_canister/main.mo",
      "args": "--incremental-gc"
    }
  }
}

Debugging

import Debug "mo:base/Debug";

Debug.print("Simple message");
Debug.print(debug_show(myComplexValue));  // Show any value

// Trap with message
assert(condition);  // Traps if false
Debug.trap("Error message");

Best Practices

Practice Rationale
Use persistent actor (EOP) All state survives upgrades automatically
Use query for read-only functions 100x faster, no consensus needed
Prefer Result over exceptions Forces callers to handle errors explicitly
Validate msg.caller for sensitive ops Prevents unauthorized access
Use inspect for DoS protection Reject bad requests before execution
Use async* for helper functions Avoids message overhead of async
Use pipe operator for chains Improves readability of transformations
Initialize vars with defaults Ensures valid state on first deploy
Use assert for invariants Fail fast on invalid state
Test upgrades locally Catch compatibility issues early

Additional Resources

In this skill:
- patterns.md - Data structures (List, OrderedMap, stable maps), HTTP handling, cryptography, encoding, mops packages, inter-canister calls, cycles management, deduplication
- style.md - Idiomatic style guide, naming conventions, functional patterns, pipe operator idioms, async* patterns
- advanced.md - Modules, shared types, atomicity, regions, management canister, upgrade migrations, EOP migration, graph-copy stabilization, canister snapshots, troubleshooting
- testing.md - mops test framework, benchmarking with mops bench, PocketIC integration testing, e2e testing, CI setup

External documentation:
- Motoko Language Manual
- Base Library Reference
- Motoko Playground
- mops Package Registry - Community packages

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.