How do I upgrade child canisters that were dynamically created from a parent canister of which I am the controlller (In Motoko)?

I run the script locally :sweat_smile:

Any server or CI that has NodeJS and a filesystem context can execute fs.readFileSync.

1 Like

Ahhh. That makes sense. Thanks for explaining :pray:t5:.

1 Like

My pleasure, keep me posted

ok. I think I screwed something up. Now I’m getting this error in the browser console:

index.js:2 Uncaught (in promise) Error: Call was rejected:
  Request ID: b7118a33259f089463a9c200c6d6e3223bb1f03043d8600f665f6549fc614b09
  Reject code: 5
  Reject text: Canister cxi6d-5iaaa-aaaap-qaaka-cai trapped explicitly: IDL error: too few arguments v(Nr(date:t,draft:b,emailOne:t,emailThree:t,emailTwo:t,entryTitle:t,file1MetaData:6=r(fileName:t,fileType:t,lastModified:I),file2MetaData:!6,location:t,lockTime:I,read:b,sent:b,text:t,unlockTime:I))r(dedications:t,dob:t,name:t,pob:t,preface:t)

This comes after I updated the child canisters using the strategy that you suggested @peterparker. Any idea why I would be getting a too few arguments error? and how I’d resolve this? The canisters did upgrade without throwing any errors. But now, when I try to call the backend to retrieve the data within the canister, this error pops up.

My code:
as I previously mentioned, I decided to create an interface on my frontend that allows me to upload the wasm file and press a button that takes that wasm file and performs the necessary procedures.

front end:

import { IDL } from "@dfinity/candid";


const handleUpgrade = async () => {

        let promises =[];

        const wasmModule = await loadWasm();

        const principalsList = await actor.getPrincipalsList();

        principalsList.forEach((principal) => promises.push(upgradeJournalData(principal, wasmModule)));

        await Promise.all(promises);

        console.log("wasm module: ",wasmModule);
    };
const upgradeJournalData = async (principal, wasmModule) => {

        console.log(`Upgrading: ${principal.toText()}`);
        const arg = IDL.encode([IDL.Principal], [principal]);
        await actor.installCode(principal, [...arg], wasmModule);
        console.log(`Done: ${principal.toText()}`);

    };

function that takes the wasm file buffer and returns a Uint8Array from it.

const loadWasm = async () => {

        const buffer = await getArrayBuffer(inputRef.current.files[0]);
        return [...new Uint8Array(buffer)];
    };

function that takes the wasm file and returns the buffer from it:

const getArrayBuffer = (inputFile) => {
        const reader = new FileReader();

        return new Promise((resolve, reject) => {
            reader.onload = () => {
                resolve(reader.result);
            }
            reader.readAsArrayBuffer(inputFile)
        
        });
    }; 

Back end

public shared(msg) func installCode( userPrincipal: Principal, args: Blob, wasmModule: Blob): async() {
        let callerId = msg.caller;

        let profile = Trie.find(
            profiles,
            key(callerId),
            Principal.equal
        );

        switch(profile){
            case null{
                throw Error.reject("Unauthorized access. Caller is not an admin.");
            };
            case ( ? existingProfile){

                if (Option.get(existingProfile.userName, "null") == "admin") {

                    let theUserProfile = Trie.find(
                        profiles,
                        key(userPrincipal),
                        Principal.equal
                    );

                    switch(theUserProfile){
                        case null{
                            throw Error.reject("No profile for this principal.");
                        };
                        case ( ? existingProfile){
                            let userJournal = existingProfile.journal;
                            let journalCanisterId = Principal.fromActor(userJournal);

                            await ic.install_code({
                                arg = args;
                                wasm_module = wasmModule;
                                mode = #upgrade;
                                canister_id = journalCanisterId;
                            });

                        };
                    };

                } else {
                    throw Error.reject("Unauthorized access. Caller is not an admin.");

                }

            };

        };
    };

The canister Id that I passed into the ic.install_code function is the principal of the user’s canister actor. when the users’ canisters are created from the actor class, their principal is passed in as an argument.

1 Like

Does your child canister as a principal as parameter or more parameters?

In above piece of code you path one principal as argument for the install code operation. This matches for example the definition in Motoko of a child actor such as

actor class DataBucket(owner: Principal) = this {
};

So if you have more or less arguments, that’s maybe the root cause of the issue?

my child canister class has the following signature:

shared(msg) actor class Journal (principal : Principal) = this {}

Maybe the shared keyword needs to be accounted for in the IDL.ecode() function? If so, how would i do that?

Also, the wasm module that i passed into the install_code() method is the wasm module i retrieved by taking my code, and deploying it to another canister on the IC. The code works just fine on the IC if it’s a newly deployed canister. So i took the backend canister’s wasm module module of the newly deployed canister and used that to pass into the install_code function to upgrade the child canisters of my already-deployed canisters running on the IC

Not sure but I would say probably not.

Did you select the main.mo or the Journal.mo wasm code to install the code (wasmModule)?

Good question. I wasn’t aware that the Journal.mo had its own wasm file. Where do i find it?

In the .dfx/ic/canisters Directory, i see three folders: one for my backend, one for my front end and one titled “idl”. The one titled is empty, and the the folder for my backend has only one wasm file. That’s the wasm file i used

also in .dfx in a folder next to your main.mo wasm code

however it might not always present and not always up-to-date with the last build depending of the settings. there is probably a nice way to generate it with a dfx command line but I am not big with the command line, so I generate it with a workaround.

  1. temporarly, in dfx.json add the Journal.mo
  2. dfx build or dfx deploy
  3. above command error will ends in error but it’s alright
  4. you have the wasm code

don’t forget to revert the change in dfx.json when finished.

with 1. I mean for example following. this is my original config

"canisters": {
    "manager": {
      "main": "canisters/src/manager/manager.mo",
      "type": "motoko"
    }
  }

then I add my child canister just to generate the wasm code

"canisters": {
    "manager": {
      "main": "canisters/src/manager/manager.mo",
      "type": "motoko"
    },
    "child": {
      "main": "canisters/src/child/child.mo",
      "type": "motoko"
    }
  }

again there is probably a way to do this by dfx but above generate the wasm code but also the did files what is often handy.

1 Like

Gonna give this a try now

@peterparker , you’re my hero. It worked! i can finally go to bed. its 3:30 AM where I am :smiling_face_with_tear:

2 Likes

awesome, happy to hear that :+1:

3:30 am, such a dedication! sleep well

1 Like

Just to clary, the wasm for an imported actor class is actually embedded in the wasm of the importing code.

Adding the additional compilation target to the dfx.json, (thus compiling the imported class on its own) has the side-effect of producing the wasm of the imported class as a separate, non-embedded thing.

As far as I know, that’s the only way to do this just now.

I’ve considered making the wasm of an imported class programmatically accessible as a blob somehow, or, alternatively, providing additional functions to perform manual installation/upgrade with all IC parameters exposed in the imported library (in addition to the class constructor), but they all seem like hacky solutions and are difficult to make type safe.

4 Likes

Thanks for the explanation Claudio :+1:

About the “hacky solutions”, no rush for me, I can live with my current “hacky” solution :wink:.

1 Like

Are there any breaking/differing behavior or “got-yas” once a canister is upgraded in the way @peterparker has outlined?

For example, if I would imagine that if I upgrade each of the canisters independently in this way, I would also need to redeploy the main canister as well so that it also has the new correct child canister wasm embedded in it in order to allow the main canister to continue dynamically spawning more child canisters.

Are there any additional side effects to keep in mind when updating settings, uninstalling, stopping, or deleting canisters through the main canister after such an upgrade of all the child canisters?

Hello Claudio!
I was blocked when installing wasm module.
If I install wasm file using ic-install_code interface, such as the following

let install_result = await IC_actor.install_code({
            arg = Blob.toArray(some_args));
            wasm_module = Blob.toArray(wasm);
            mode = #reinstall;
            canister_id = new_canister_id;
        });

This canister will trapped

trapped explicitly: Custom(Trailing value after finishing deserialization

when i call a function which inputs is an nested data.

Well,if i install this wasm and specific a candid file using dfx command with dfx.json config, it works fine.
What should i do when using ic-install_code interface?
Seems it’s a type problem.

Not sure, but can you show me:

  • the Candid or Motoko type of the argument to the installed code?
  • the way in which you obtain the blob for the arguments?
  • the dfx command that works?

Here I have collect something:

  1. the Candid or Motoko type of the argument to the installed code
    just normal init arguments, it can be null type. It does not affect the call operation.

  2. the way in which you obtain the blob for the arguments
    wasm load function with node js:

import {readFileSync} from 'fs';
​
const loadWasm = () => {
  const localPath = 
     `${process.cwd()}/.dfx/local/canisters/bucket/bucket.wasm`;
  const buffer = readFileSync(localPath);
  return [...new Uint8Array(buffer)];
};
  1. the dfx command that works
    The succeed command
dfx --identity NFT_trader_E canister call tlwi3-3aaaa-aaaaa-aaapq-cai mint '(principal "ome-principal",    7328:nat, vec { 
    record { "image"; variant { TextContent = "0.jpg" } }; 
    record { "tokenId"; variant { TextContent = "0" } }; 
    record { "name"; variant { TextContent = "Corn #0" } }; 
    record { "description"; variant { TextContent = "Corn #0" } } 
} )'

The failed command

dfx --identity NFT_trader_E canister call tlwi3-3aaaa-aaaaa-aaapq-cai mint '(principal "some-principal",    7326:nat, vec { 
    record { "image"; variant { TextContent = "0.jpg"}   };
    record { "tokenId"; variant { TextContent = "0"}   };
    record { "name"; variant { TextContent = "Corn #0"}   };
    record { "description"; variant { TextContent = "Corn #0"}   };
    record { "attributes"; 
        variant { 
            NestedContent = vec { 
                record { "Background"; variant { TextContent = "light"}   };
                record { "Name"; variant { TextContent = "Chon"}   } 
            } 
        } 
    } 
} )'
  1. The type about mint:
type GenericValue = variant {
  Nat64Content : nat64;
  Nat32Content : nat32;
  BoolContent : bool;
  Nat8Content : nat8;
  Int64Content : int64;
  IntContent : int;
  NatContent : nat;
  Nat16Content : nat16;
  Int32Content : int32;
  Int8Content : int8;
  FloatContent : float64;
  Int16Content : int16;
  BlobContent : vec nat8;
  NestedContent : Vec;
  Principal : principal;
  TextContent : text;
};

type Vec = vec record {
  text;
  variant {
    Nat64Content : nat64;
    Nat32Content : nat32;
    BoolContent : bool;
    Nat8Content : nat8;
    Int64Content : int64;
    IntContent : int;
    NatContent : nat;
    Nat16Content : nat16;
    Int32Content : int32;
    Int8Content : int8;
    FloatContent : float64;
    Int16Content : int16;
    BlobContent : vec nat8;
    NestedContent : Vec;
    Principal : principal;
    TextContent : text;
  };
};

mint : (principal, nat, vec record { text; GenericValue }) -> (Result);
  1. The error info
Error: Invalid vec record {text; variant {Nat64Content:nat64; Nat32Content:nat32; BoolContent:bool; Nat8Content:nat8; Int64Content:int64; IntContent:int; NatContent:nat; Nat16Content:nat16; Int32Content:int32; Int8Content:int8; FloatContent:float64; Int16Content:int16; BlobContent:vec nat8; NestedContent:μrec_2.vec record {_0_:text; _1_:variant {Nat64Content:nat64; Nat32Content:nat32; BoolContent:bool; Nat8Content:nat8; Int64Content:int64; IntContent:int; NatContent:nat; Nat16Content:nat16; Int32Content:int32; Int8Content:int8; FloatContent:float64; Int16Content:int16; BlobContent:vec nat8; NestedContent:rec_2; Principal:principal; TextContent:text}}; Principal:principal; TextContent:text}} argument: "vec { record { \"image\"; variant { TextContent = \"1.jpg\" } }; record { \"tokenId\"; variant { TextContent = \"1\" } }; record { \"name\"; variant { TextContent = \"Corn #1\" } }; record { \"description\"; variant { TextContent = \"Corn #1\" } }; }"

I can’t see the error, but spacing out your error message I see:

Error: Invalid
vec record {
    text;
    variant {
       Nat64Content:nat64;
       Nat32Content:nat32;
       BoolContent:bool;
       Nat8Content:nat8;
       Int64Content:int64;
       IntContent:int;
       NatContent:nat;
       Nat16Content:nat16;
       Int32Content:int32;
       Int8Content:int8;
       FloatContent:float64;
       Int16Content:int16;
       BlobContent:vec nat8;
       NestedContent:μrec_2.vec record {
         _0_: text;
         _1_: variant {
	     Nat64Content:nat64; Nat32Content:nat32;
	     BoolContent:bool; Nat8Content:nat8; Int64Content:int64;
	     IntContent:int; NatContent:nat; Nat16Content:nat16; Int32Content:int32; Int8Content:int8;
	     FloatContent:float64; Int16Content:int16; BlobContent:vec nat8; NestedContent:rec_2;
	     Principal:principal; TextContent:text}
       };
       Principal:principal;
       TextContent:text
   }
} argument:
"vec { record { \"image\"; variant { TextContent = \"1.jpg\" } }; 
       record { \"tokenId\"; variant { TextContent = \"1\" } }; 
       record { \"name\"; variant { TextContent = \"Corn #1\" } };
       record { \"description\"; variant { TextContent = \"Corn #1\" } }; 
}"

@chenyan can you see anything wrong with this? I rarely use candid values directly.

So the install_code call does succeed, and the problem is only when calling the mint method, right?

I think what’s happening is that dfx call with canister_id (instead of a project name specified in dfx.json) doesn’t fetch the candid file when making calls. Therefore, some of the type isn’t inferred properly. Can you try dfx canister call tlwi3-3aaaa-aaaaa-aaapq-cai mint '...' --candid your_candid_file.did and see if it works?

1 Like