Cool, then this should work. There are, however, some caveats. On the spawner canister you have to keep track of the principals you want to add as controllers, and do it in 2 places, to maintain further functionality with dfx / direct calling of the newly spawned canister.
In order to spawn a new canister the spawner canister needs to make 2 calls to the management canister: create_canister
and install_code
.
First, in order for the system (i.e. the management canister) to register a principal as a controller of a canister (with full controller powers) it needs to be added in the create_canister
call.
#[derive(CandidType, Debug, Clone, Deserialize)]
pub struct CreateCanisterSettings {
pub controllers: Option<Vec<Principal>>, //<--- HERE
pub compute_allocation: Option<Nat>,
pub memory_allocation: Option<Nat>,
pub freezing_threshold: Option<Nat>,
}
#[derive(CandidType, Clone, Deserialize)]
pub struct CreateCanisterArgs {
pub cycles: u64,
pub settings: CreateCanisterSettings,
}
Whatever controllers you add here will have full controller status (that means they will be able to directly call the canister from dfx / whatever).
One issue is that the newly spawned canister can only receive arguments from the install_code (I think). So we need to also add the same controllers to the install_code
call.
#[derive(CandidType, Deserialize)]
enum InstallMode {
#[serde(rename = "install")]
Install,
#[serde(rename = "reinstall")]
Reinstall,
#[serde(rename = "upgrade")]
Upgrade,
}
#[derive(CandidType, Deserialize)]
struct CanisterInstall {
mode: InstallMode,
canister_id: Principal,
#[serde(with = "serde_bytes")]
wasm_module: Vec<u8>,
#[serde(with = "serde_bytes")]
arg: Vec<u8>, // <-- HERE
}
This “arg” gets passed down to the newly spawned canister, and it can read it from init() (as a sidetrack, also from pre_ and post_upgrade).
The way I managed to send this was like so:
#[derive(CandidType, Deserialize)]
struct CanisterInstallSendArgs {
greet: String,
controllers: Vec<Principal>,
}
let canister_install_args = Encode!(&CanisterInstallSendArgs {
greet: "Hello from Index".to_string(),
controllers: vec![Principal::from_text(
"l6s27-7ndcl-nowe5-xeyf7-ymdnq-dkemz-jkhfw-zr5wu-jvf2p-aupzq-2qe",
)
.unwrap(),],
})
.unwrap();
Where CanisterInstallSendArgs
is something you define. I added a “greet” there just to test things out. This struct is what you get on the other side, from init().
Keep in mind that it’s on you to make sure the principal vecs look the same in both calls.
Also, as I mentioned previously, if someone adds a new controller via dfx it will be missed by this approach.
Speaking of init(), there’s a prettier way of accessing the args than that ugly thing I used first time Check out this post for details. tl;dr; you can decorate the init() function with the #[candid_method(init)]
macro, and the service will be correctly interpreted in the .did file and allow you to send install_code arguments from dfx as well. (rust to rust would work with just what I described above)