​How to Migrate Canister Smart Contracts from Motoko to Rust

I migrated 500+ canisters from Motoko to Rust on the Internet Computer. Here are the two major things I learned.

Photo by Pawel Czerwinski on Unsplash

1. Creating Rust canister on the fly

// src/motoko_canister/main.mo

import RustCanister "canister:rust_canister";

actor Main {

public shared ({ caller }) func hello() : async (Principal) {
let canisterId = await RustCanister.world(caller);
return canisterId;
};

}

// src/rust_canister/src/lib.rs

use candid::Principal;
use ic_cdk_macros::{update};
use ic_cdk::{api};

#[update]
async fn create_bucket(_user_id: Principal) -> Principal {
let caller = api::caller();
caller
}
import Cycles "mo:base/ExperimentalCycles";
import Principal "mo:base/Principal";
import Blob "mo:base/Blob";

// types for the IC. e.g. available in https://github.com/papyrs/ic
import IC "./ic.types";

actor Main {

private let ic : IC.Self = actor "aaaaa-aa";

private stable var storageWasm: [Nat8] = [];

public shared ({ caller }) func init() : async (Principal) {
// Indicates the cycles to be transferred in the next call
Cycles.add(1_000_000_000_000);

// Create a new canister
let { canister_id } = await ic.create_canister({ settings = null });

// Set the controllers of the new canister
// In this case this canister, the caller and the canister itself
let self : Principal = Principal.fromActor(Main);
let controllers : ?[Principal] = ?[canister_id, caller, self];

// Update the settings to apply the controllers
await ic.update_settings(({
canister_id;
settings = {
controllers = controllers;
freezing_threshold = null;
memory_allocation = null;
compute_allocation = null;
};
}));

// Example. to_candid can be used to encode initial args
let arg: Blob = to_candid(caller);

// Finally install the generic wasm code
await ic.install_code({
arg;
wasm_module = Blob.fromArray(storageWasm);
mode = #install;
canister_id;
});

return canister_id;
};
}
actor Main {

private stable var storageWasm: [Nat8] = [];

public shared ({ caller }) func storateResetWasm(): async () {
// Reject invalid caller
storageWasm := [];
};

public shared ({ caller }) func storageLoadWasm(blob: [Nat8]): async ({total: Nat; chunks: Nat;}) {
// Reject invalid caller

// Note: Array.append is deprecated but buffer.append needs dfx v12
// Issue: https://forum.dfinity.org/t/array-to-buffer-in-motoko/15880/15

storageWasm := Array.append<Nat8>(storageWasm, blob);

// Return total wasm sizes
return {
total = storageWasm.size();
chunks = blob.size();
}
};
};
import { readFile } from "fs/promises";
import {canisterId, managerActor} from "./manager.actor.mjs";

const loadWasm = async () => {
const buffer = await readFile(
`${process.cwd()}/.dfx/local/canisters/my_rust_canister/my_rust_canister.wasm`
);
return [...new Uint8Array(buffer)];
};

const resetWasm = async () => {
await managerActor.storateResetWasm();
}

const installWasm = async (wasmModule) => {
console.log(`Installing wasm code in: ${canisterId}`);

const chunkSize = 700000;

const upload = async (chunks) => {
const result = await managerActor.storageLoadWasm(chunks);
console.log("Chunks:", result);
};

for (let start = 0; start < wasmModule.length; start += chunkSize) {
const chunks = wasmModule.slice(start, start + chunkSize);
await upload(chunks);
}

console.log(`Done: ${canisterId}`);
};

(async () => {
const wasmModule = await loadWasm();

// Install wasm in manager
await resetWasm();
await installWasm(wasmModule);
})();

​2. Upgrading and preserving state

import Time "mo:base/Time";
import Blob "mo:base/Blob";
import Text "mo:base/Text";

actor Demo {

private stable var test: Nat = 666;

private type Asset = {
key : Text;
modified : Int;
contentChunks: [[Nat8]];
};

private stable var entries: [(Text, Asset)] = [];

system func preupgrade() {
entries := [("yolo", {
key = "hello";
modified = Time.now();
contentChunks = [Blob.toArray(Text.encodeUtf8("world"))];
})]
};

system func postupgrade() {
// Postupgrade will happens in Rust.
// Memory has to be decoded there.
};

};
mod types;

use ic_cdk::{api::{ stable:: { stable_read } }};
use candid::{decode_args};
use ic_cdk_macros::{post_upgrade};
use std::cell::RefCell;

// State is a custom type
use crate::types::{demo::{State}};

thread_local! {
static STATE: RefCell<State> = RefCell::default();
}

#[post_upgrade]
fn post_upgrade() {
// By senior.joinu - not all heroes wear capes

// BEGIN: read the stable memory in a buffer
let mut stable_length_buf = [0u8; std::mem::size_of::<u32>()];
stable_read(0, &mut stable_length_buf);
let stable_length = u32::from_le_bytes(stable_length_buf);

let mut buf = vec![0u8; stable_length as usize];
stable_read(std::mem::size_of::<u32>() as u32, &mut buf);
// END: read

// Decode the memory buffer
let (state,): (State,) = decode_args(&buf).unwrap();

// e.g. populate state
let new_state: State = State {
test: state.test,
entries: state.entries
};

STATE.with(|state| *state.borrow_mut() = new_state);
}
pub mod demo {
use candid::{CandidType, Int};
use serde::{Serialize, Deserialize};

#[derive(CandidType, Deserialize, Serialize)]
pub struct Asset {
pub key: String,
pub modified: Int,
pub contentChunks: Vec<Vec<u8>>,
}

#[derive(Default, CandidType, Deserialize, Serialize)]
pub struct State {
pub test: Option<u128>,
pub entries: Option<Vec<(String, Asset)>>,
}
}

​Sample repo

​Summary

For more adventures, follow me on Twitter 🖖

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store