Photo by Milad Fakurian on Unsplash
On Papyrs - a web3 open source blogging platform - each user gets two smart contract canisters upon sign-in. One that contains private data and another that enables the user's personal blog-space on the internet.
Until the day I (hopefully) hand over the control of all canisters to a Sns and the community, I might have to install new version of the code in users' smart contracts by my self - e.g. to fix issues (π ) or to deploy new features (π).
This article describes how I can install code with NodeJS scripts and how you could do as well.
Getting started
Earlier this year I published two related articles:
These articles lead to this tutorial. The first display how to query canisters in NodeJS and the second how to create smart contracts on the fly - i.e. how to create canisters in which, I want to install newer version of my code π.
Child canister
I use the first of the two above posts to create a local sample project. After compilation and deployment - to a local simulated IC network - I open my browser and create on the fly a child canister renrk-eyaaa-aaaaa-aaada-cai
.
This sample smart contract is the one I aim to update in following chapters. That is why I bump its version by modifying its source code as following:
import Nat "mo:base/Nat";
actor class Bucket(user: Text) = this {
var version: Nat = 2; // <-- Bump v2
public query func say() : async Text {
return "Hello World - " # user # " - v" # Nat.toText(version);
};
}
Once modified, I have to re-generate the wasm binary that will be installed - deployed to the IC. To do so, I have to follow the workaround I shared in my previous post because, currently, there is "no other way of producing the wasm of the imported class as a separate, non-embedded thing".
Edit the configuration
dfx.json
to list the bucket actor.Run the
dfx deploy
command to generate the files. The command will end in error ("Error: Invalid data: Expected arguments but found none.") that can safely be ignored π.Revert the change in
dfx.json
.
Backend
Only controllers of the canister can install new version of the code. As the child canisters are created by a main actor - which I named manager
- I had to take care to add its principal to the list of controllers while updating the settings in my previous tutorials.
So in this solution, this actor will install the code and the NodeJS script will "only" be a caller.
The backend feature to install code - install\_code
- is part of the IC interface specification. Therefore, I can add a function to my manager
that acts as a proxy which receives the information and calls that core feature of the IC.
Important note: following code snippet is a public function! If you implement such a feature in your smart contracts on mainnet
, please apply the appropriate safety precautions.
import IC "./ic.types";
actor Main {
private let ic : IC.Self = actor "aaaaa-aa";
public func installCode(canisterId: Principal, arg: Blob, wasmModule: Blob): async() {
await ic.install_code({
arg = arg;
wasm_module = wasmModule;
mode = #upgrade;
canister_id = canisterId;
});
};
};
To install code in my target canister, I need four parameters:
- a target canister id
- the wasm module - the new version of the wasm code I built in previous chapter with my workaround
- a
mode
set to#upgrade
to perform an update as described in Canister upgrades - with the goal to maintain the state - arguments - those that are used to initialize the canister
NodeJS script
I can implement the call to the endpoint of the manager
in a NodeJS module script I named installcode.mjs
. The script will take care of collecting the parameters mentioned above before effectively calling my actor (function upgradeBucket
).
import { Principal } from "@dfinity/principal";
import { IDL } from "@dfinity/candid";
const installCode = async () => {
// Param 1.
const canisterId = Principal.fromText("renrk-eyaaa-aaaaa-aaada-cai");
// Param 2.
const wasmModule = loadWasm();
// Param 3.
const arg = IDL.encode([IDL.Text], ["User1"]);
// Agent-js actor
const actor = await managerActor();
// Execute
await upgradeBucket({ actor, wasmModule, canisterId, arg });
};
try {
await installCode();
} catch (err) {
console.error(err);
}
The first parameter is the targeted canister id as Principal
. As I collected the local child canister as a string
when I printed its id - renrk-eyaaa-aaaaa-aaada-cai
- in the browser console, I need to convert it the help of Principal.fromText()
.
The second parameter I need is the wasm module. To collect it, I can read the file that has been generated when I previously ran dfx deploy
and can transform it to an ArrayBuffer
- the expected type that matches to the Blob
defined in the backend actor's code.
import { readFileSync } from "fs";
const loadWasm = () => {
const localPath = `${process.cwd()}/.dfx/local/canisters/bucket/bucket.wasm`;
const buffer = readFileSync(localPath);
return [...new Uint8Array(buffer)];
};
The third parameter is the one that matches those use to create the canister on the fly π€ͺ. Concretely, the bucket's actors of this tutorial are created with a user
parameter:
actor class Bucket(user: Text) = this {
// commented
}
So, to install the code, I need to provide the same parameters which has to be encoded with Candid (otherwise the parameters are rejected):
import { IDL } from "@dfinity/candid";
const arg = IDL.encode([IDL.Text], ["User1"]);
Note that IDL
support various format - e.g. if the Motoko parameter would have been a Principal
, I could have encoded it as following:
import { IDL } from "@dfinity/candid";
import { Principal } from "@dfinity/principal";
const arg = IDL.encode([IDL.Principal], [Principal.fromText("rrrrr-ccccc-user-principal")]);
To instantiate the manager
actor, once I find its canister ID, I can proceed as I would commonly do with agent-js:
import { idlFactory } from "./.dfx/local/canisters/manager/manager.did.mjs";
import fetch from "node-fetch";
import { HttpAgent, Actor } from "@dfinity/agent";
const managerActor = async () => {
const canisterId = managerPrincipalLocal();
// Replace host with https://ic0.app for mainnet
const agent = new HttpAgent({ fetch, host: "http://localhost:8000/" });
// Only if local IC
await agent.fetchRootKey();
return Actor.createActor(idlFactory, {
agent,
canisterId
});
};
However, there is one subtlety: because I am writing a module script - .mjs
- I cannot import
the idlFactory
script that was automatically generated by dfx
as a .js
file.
To overcome this issue, I just had to copy it to change its extension. Fortunately this does the trick.
cp ./.dfx/local/canisters/manager/manager.did.js ./.dfx/local/canisters/manager/manager.did.mjs
The principal ID of the manager
deployed on a local simulated IC can be found in the .dfx
folder.
const managerPrincipalLocal = () => {
const buffer = readFileSync("./.dfx/local/canister_ids.json");
const { manager } = JSON.parse(buffer.toString("utf-8"));
return Principal.fromText(manager.local);
};
Ultimately, if you would deploy on mainnet
, you would be able to find the same information in the canister\_ids.json
present at the root of your project.
const managerPrincipalIC = () => {
const buffer = readFileSync("./canister_ids.json");
const { manager } = JSON.parse(buffer.toString("utf-8"));
return Principal.fromText(manager.ic);
};
Note that you would also have to comment fetchRootKey
and change the host
property in the HttpAgent
initialization.
Finally, the effective call that will install the code can be implemented with the parameters I collected.
const upgradeBucket = async ({ actor, wasmModule, canisterId, arg }) => {
console.log(`Upgrading: ${canisterId.toText()}`);
await actor.installCode(canisterId, [...arg], wasmModule);
console.log(`Done: ${canisterId.toText()}`);
};
Test
Everything is set. I can call my NodeJS script - node installcode.mjs
.
The installation was a success. To be certain the code was deployed, I called afterwards the canister - which was updated - to check that indeed, it now returned the new version - v2
- I was expecting and, indeed it worked out π.
Conclusion and sample repo
You can find the source code of this tutorial in a sample repo I published on GitHub:
π https://github.com/peterpeterparker/manager
I hope it will be useful for the community and let me know if you have idea of improvements!
To infinity and beyond
David