1use crate::dfx;
2use crate::release_set::{
3 configured_fleet_name, configured_install_targets, dfx_call, dfx_root,
4 emit_root_release_set_manifest_with_config, load_root_release_set_manifest,
5 resume_root_bootstrap, stage_root_release_set, workspace_root,
6};
7use canic_core::{cdk::types::Principal, protocol};
8use config_selection::resolve_install_config_path;
9use serde::Deserialize;
10use serde_json::Value;
11use std::{
12 env,
13 path::Path,
14 process::Command,
15 thread,
16 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
17};
18
19mod config_selection;
20mod state;
21
22pub use state::{
23 FleetSummary, InstallState, list_current_fleets, read_current_install_state,
24 read_current_or_fleet_install_state, select_current_fleet,
25};
26use state::{INSTALL_STATE_SCHEMA_VERSION, validate_fleet_name, write_install_state};
27
28#[cfg(test)]
29mod tests;
30
31#[cfg(test)]
32use config_selection::{config_selection_error, discover_canic_config_choices};
33#[cfg(test)]
34use state::{
35 current_fleet_path, fleet_install_state_path, list_fleets, read_fleet_install_state,
36 read_install_state,
37};
38
39#[derive(Clone, Debug)]
44pub struct InstallRootOptions {
45 pub root_canister: String,
46 pub root_build_target: String,
47 pub network: String,
48 pub ready_timeout_seconds: u64,
49 pub config_path: Option<String>,
50 pub interactive_config_selection: bool,
51}
52
53#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
58struct BootstrapStatusSnapshot {
59 ready: bool,
60 phase: String,
61 last_error: Option<String>,
62}
63
64#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
69struct InstallTimingSummary {
70 create_canisters: Duration,
71 build_all: Duration,
72 emit_manifest: Duration,
73 fabricate_cycles: Duration,
74 install_root: Duration,
75 stage_release_set: Duration,
76 resume_bootstrap: Duration,
77 wait_ready: Duration,
78}
79
80const LOCAL_ROOT_TARGET_CYCLES: u128 = 9_000_000_000_000_000;
81const LOCAL_DFX_READY_TIMEOUT_SECONDS: u64 = 30;
82
83pub fn install_root(options: InstallRootOptions) -> Result<(), Box<dyn std::error::Error>> {
85 let workspace_root = workspace_root()?;
86 let dfx_root = dfx_root()?;
87 let config_path = resolve_install_config_path(
88 &workspace_root,
89 options.config_path.as_deref(),
90 options.interactive_config_selection,
91 )?;
92 let fleet_name = configured_fleet_name(&config_path)?;
93 validate_fleet_name(&fleet_name)?;
94 let total_started_at = Instant::now();
95 let mut timings = InstallTimingSummary::default();
96
97 println!(
98 "Installing fleet {} against DFX_NETWORK={}",
99 fleet_name, options.network
100 );
101 ensure_dfx_running(&dfx_root, &options.network)?;
102 let mut create = Command::new("dfx");
103 create
104 .current_dir(&dfx_root)
105 .args(["canister", "create", "--all", "-qq"]);
106 let create_started_at = Instant::now();
107 run_command(&mut create)?;
108 timings.create_canisters = create_started_at.elapsed();
109
110 let build_targets = configured_install_targets(&config_path, &options.root_build_target)?;
111 let build_session_id = install_build_session_id();
112 let build_started_at = Instant::now();
113 run_dfx_build_targets(&dfx_root, &build_targets, &build_session_id, &config_path)?;
114 timings.build_all = build_started_at.elapsed();
115
116 let emit_manifest_started_at = Instant::now();
117 let manifest_path = emit_root_release_set_manifest_with_config(
118 &workspace_root,
119 &dfx_root,
120 &options.network,
121 &config_path,
122 )?;
123 timings.emit_manifest = emit_manifest_started_at.elapsed();
124
125 timings.fabricate_cycles =
126 maybe_fabricate_local_cycles(&dfx_root, &options.root_canister, &options.network)?;
127
128 let mut install = Command::new("dfx");
129 install.current_dir(&dfx_root).args([
130 "canister",
131 "install",
132 &options.root_canister,
133 "--mode=reinstall",
134 "-y",
135 "--argument",
136 "(variant { Prime })",
137 ]);
138 let install_started_at = Instant::now();
139 run_command(&mut install)?;
140 timings.install_root = install_started_at.elapsed();
141
142 let manifest = load_root_release_set_manifest(&manifest_path)?;
143 let stage_started_at = Instant::now();
144 stage_root_release_set(&dfx_root, &options.root_canister, &manifest)?;
145 timings.stage_release_set = stage_started_at.elapsed();
146 let resume_started_at = Instant::now();
147 resume_root_bootstrap(&options.root_canister)?;
148 timings.resume_bootstrap = resume_started_at.elapsed();
149 let ready_started_at = Instant::now();
150 let ready_result = wait_for_root_ready(&options.root_canister, options.ready_timeout_seconds);
151 timings.wait_ready = ready_started_at.elapsed();
152 if let Err(err) = ready_result {
153 print_install_timing_summary(&timings, total_started_at.elapsed());
154 return Err(err);
155 }
156
157 print_install_timing_summary(&timings, total_started_at.elapsed());
158 let state = build_install_state(
159 &options,
160 &workspace_root,
161 &dfx_root,
162 &config_path,
163 &manifest_path,
164 &fleet_name,
165 )?;
166 let state_path = write_install_state(&dfx_root, &options.network, &state)?;
167 print_install_result_summary(&options.network, &state.fleet, &state_path);
168 Ok(())
169}
170
171fn build_install_state(
173 options: &InstallRootOptions,
174 workspace_root: &Path,
175 dfx_root: &Path,
176 config_path: &Path,
177 release_set_manifest_path: &Path,
178 fleet_name: &str,
179) -> Result<InstallState, Box<dyn std::error::Error>> {
180 Ok(InstallState {
181 schema_version: INSTALL_STATE_SCHEMA_VERSION,
182 fleet: fleet_name.to_string(),
183 installed_at_unix_secs: current_unix_secs()?,
184 network: options.network.clone(),
185 root_target: options.root_canister.clone(),
186 root_canister_id: resolve_root_canister_id(dfx_root, &options.root_canister)?,
187 root_build_target: options.root_build_target.clone(),
188 workspace_root: workspace_root.display().to_string(),
189 dfx_root: dfx_root.display().to_string(),
190 config_path: config_path.display().to_string(),
191 release_set_manifest_path: release_set_manifest_path.display().to_string(),
192 })
193}
194
195fn resolve_root_canister_id(
197 dfx_root: &Path,
198 root_canister: &str,
199) -> Result<String, Box<dyn std::error::Error>> {
200 if Principal::from_text(root_canister).is_ok() {
201 return Ok(root_canister.to_string());
202 }
203
204 let mut command = Command::new("dfx");
205 command
206 .current_dir(dfx_root)
207 .args(["canister", "id", root_canister]);
208 Ok(run_command_stdout(&mut command)?.trim().to_string())
209}
210
211fn current_unix_secs() -> Result<u64, Box<dyn std::error::Error>> {
213 Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
214}
215
216fn run_dfx_build_targets(
218 dfx_root: &Path,
219 targets: &[String],
220 build_session_id: &str,
221 config_path: &Path,
222) -> Result<(), Box<dyn std::error::Error>> {
223 println!("Build artifacts:");
224 println!("{:<16} {:<18} {:>10}", "CANISTER", "PROGRESS", "ELAPSED");
225
226 for (index, target) in targets.iter().enumerate() {
227 let mut command = dfx_build_target_command(dfx_root, target, build_session_id);
228 command.env("CANIC_CONFIG_PATH", config_path);
229 let started_at = Instant::now();
230 let output = command.output()?;
231 let elapsed = started_at.elapsed();
232
233 if !output.status.success() {
234 return Err(format!(
235 "dfx build failed for {target}: {}\nstdout:\n{}\nstderr:\n{}",
236 output.status,
237 String::from_utf8_lossy(&output.stdout).trim(),
238 String::from_utf8_lossy(&output.stderr).trim()
239 )
240 .into());
241 }
242
243 println!(
244 "{:<16} {:<18} {:>9.2}s",
245 target,
246 progress_bar(index + 1, targets.len(), 10),
247 elapsed.as_secs_f64()
248 );
249 }
250
251 println!();
252 Ok(())
253}
254
255fn dfx_build_target_command(dfx_root: &Path, target: &str, build_session_id: &str) -> Command {
258 let mut command = Command::new("dfx");
259 command
260 .current_dir(dfx_root)
261 .env("CANIC_BUILD_CONTEXT_SESSION", build_session_id)
262 .args(["build", "-qq", target]);
263 command
264}
265
266fn install_build_session_id() -> String {
267 let unique = SystemTime::now()
268 .duration_since(UNIX_EPOCH)
269 .map_or(0, |duration| duration.as_nanos());
270 format!("install-root-{}-{unique}", std::process::id())
271}
272
273fn maybe_fabricate_local_cycles(
275 dfx_root: &Path,
276 root_canister: &str,
277 network: &str,
278) -> Result<Duration, Box<dyn std::error::Error>> {
279 if network != "local" {
280 return Ok(Duration::ZERO);
281 }
282
283 let current_balance = root_cycle_balance(dfx_root, root_canister)?;
284 let Some(fabricate_cycles) = required_local_cycle_topup(current_balance) else {
285 println!(
286 "Skipping local cycle fabrication for {root_canister}; balance {} already meets target {}",
287 format_cycles(current_balance),
288 format_cycles(LOCAL_ROOT_TARGET_CYCLES)
289 );
290 return Ok(Duration::ZERO);
291 };
292
293 let mut fabricate = Command::new("dfx");
294 fabricate.current_dir(dfx_root);
295 fabricate.args([
296 "ledger",
297 "fabricate-cycles",
298 "--canister",
299 root_canister,
300 "--cycles",
301 &fabricate_cycles.to_string(),
302 ]);
303 let fabricate_started_at = Instant::now();
304 let output = fabricate.output()?;
305 print_local_cycle_topup_summary(root_canister, current_balance, fabricate_cycles, &output);
306
307 Ok(fabricate_started_at.elapsed())
308}
309
310fn print_local_cycle_topup_summary(
312 root_canister: &str,
313 current_balance: u128,
314 fabricate_cycles: u128,
315 output: &std::process::Output,
316) {
317 let status = if output.status.success() {
318 "topped up"
319 } else {
320 "top-up requested"
321 };
322 println!(
323 "\n\x1b[33mcycles: {status} local root {root_canister} by {} toward target {} (was {})\x1b[0m\n",
324 format_cycles(fabricate_cycles),
325 format_cycles(LOCAL_ROOT_TARGET_CYCLES),
326 format_cycles(current_balance)
327 );
328}
329
330fn root_cycle_balance(
332 dfx_root: &Path,
333 root_canister: &str,
334) -> Result<u128, Box<dyn std::error::Error>> {
335 let mut command = Command::new("dfx");
336 command
337 .current_dir(dfx_root)
338 .args(["canister", "status", root_canister]);
339 let stdout = dfx::run_output(&mut command)?;
340 parse_canister_status_cycles(&stdout)
341 .ok_or_else(|| "could not parse cycle balance from `dfx canister status` output".into())
342}
343
344fn parse_canister_status_cycles(status_output: &str) -> Option<u128> {
346 status_output
347 .lines()
348 .find_map(parse_canister_status_balance_line)
349}
350
351fn parse_canister_status_balance_line(line: &str) -> Option<u128> {
352 let (label, value) = line.trim().split_once(':')?;
353 let label = label.trim().to_ascii_lowercase();
354 if label != "balance" && label != "cycle balance" {
355 return None;
356 }
357
358 let digits = value
359 .chars()
360 .filter(char::is_ascii_digit)
361 .collect::<String>();
362 if digits.is_empty() {
363 return None;
364 }
365
366 digits.parse::<u128>().ok()
367}
368
369fn required_local_cycle_topup(current_balance: u128) -> Option<u128> {
371 (current_balance < LOCAL_ROOT_TARGET_CYCLES)
372 .then_some(LOCAL_ROOT_TARGET_CYCLES.saturating_sub(current_balance))
373 .filter(|cycles| *cycles > 0)
374}
375
376fn format_cycles(value: u128) -> String {
377 let digits = value.to_string();
378 let mut out = String::with_capacity(digits.len() + (digits.len().saturating_sub(1) / 3));
379 for (index, ch) in digits.chars().enumerate() {
380 if index > 0 && (digits.len() - index).is_multiple_of(3) {
381 out.push('_');
382 }
383 out.push(ch);
384 }
385 out
386}
387
388fn progress_bar(current: usize, total: usize, width: usize) -> String {
389 if total == 0 || width == 0 {
390 return "[] 0/0".to_string();
391 }
392
393 let filled = current.saturating_mul(width).div_ceil(total);
394 let filled = filled.min(width);
395 format!(
396 "[{}{}] {current}/{total}",
397 "#".repeat(filled),
398 " ".repeat(width - filled)
399 )
400}
401
402fn ensure_dfx_running(dfx_root: &Path, network: &str) -> Result<(), Box<dyn std::error::Error>> {
404 if dfx_ping(network)? {
405 return Ok(());
406 }
407
408 if network == "local" && local_dfx_autostart_enabled() {
409 println!("Local dfx replica is not reachable; starting a clean local replica");
410 let mut stop = dfx_stop_command(dfx_root);
411 let _ = run_command_allow_failure(&mut stop)?;
412
413 let mut start = dfx_start_local_command(dfx_root);
414 run_command(&mut start)?;
415 wait_for_dfx_ping(
416 network,
417 Duration::from_secs(LOCAL_DFX_READY_TIMEOUT_SECONDS),
418 )?;
419 return Ok(());
420 }
421
422 Err(format!(
423 "dfx replica is not running for network '{network}'\nStart the target replica externally and rerun."
424 )
425 .into())
426}
427
428fn dfx_ping(network: &str) -> Result<bool, Box<dyn std::error::Error>> {
430 Ok(Command::new("dfx")
431 .args(["ping", network])
432 .output()?
433 .status
434 .success())
435}
436
437fn local_dfx_autostart_enabled() -> bool {
439 parse_local_dfx_autostart(env::var("CANIC_AUTO_START_LOCAL_DFX").ok().as_deref())
440}
441
442fn parse_local_dfx_autostart(value: Option<&str>) -> bool {
443 !matches!(
444 value.map(str::trim).map(str::to_ascii_lowercase).as_deref(),
445 Some("0" | "false" | "no" | "off")
446 )
447}
448
449fn dfx_stop_command(dfx_root: &Path) -> Command {
451 let mut command = Command::new("dfx");
452 command.current_dir(dfx_root).arg("stop");
453 command
454}
455
456fn dfx_start_local_command(dfx_root: &Path) -> Command {
458 let mut command = Command::new("dfx");
459 command
460 .current_dir(dfx_root)
461 .args(["start", "--background", "--clean", "--system-canisters"]);
462 command
463}
464
465fn wait_for_dfx_ping(network: &str, timeout: Duration) -> Result<(), Box<dyn std::error::Error>> {
467 let start = Instant::now();
468 while start.elapsed() < timeout {
469 if dfx_ping(network)? {
470 return Ok(());
471 }
472 thread::sleep(Duration::from_millis(500));
473 }
474
475 Err(format!(
476 "dfx replica did not become ready for network '{network}' within {}s",
477 timeout.as_secs()
478 )
479 .into())
480}
481
482fn wait_for_root_ready(
484 root_canister: &str,
485 timeout_seconds: u64,
486) -> Result<(), Box<dyn std::error::Error>> {
487 let start = std::time::Instant::now();
488 let mut next_report = 0_u64;
489
490 println!("Waiting for {root_canister} to report canic_ready (timeout {timeout_seconds}s)");
491
492 loop {
493 if root_ready(root_canister)? {
494 println!(
495 "{root_canister} reported canic_ready after {}s",
496 start.elapsed().as_secs()
497 );
498 return Ok(());
499 }
500
501 if let Some(status) = root_bootstrap_status(root_canister)?
502 && let Some(last_error) = status.last_error.as_deref()
503 {
504 eprintln!(
505 "root bootstrap reported failure during phase '{}' : {}",
506 status.phase, last_error
507 );
508 eprintln!("Diagnostic: dfx canister call {root_canister} canic_bootstrap_status");
509 print_raw_call(root_canister, protocol::CANIC_BOOTSTRAP_STATUS);
510 eprintln!("Diagnostic: dfx canister call {root_canister} canic_subnet_registry");
511 print_raw_call(root_canister, "canic_subnet_registry");
512 eprintln!(
513 "Diagnostic: dfx canister call {root_canister} canic_wasm_store_bootstrap_debug"
514 );
515 print_raw_call(root_canister, "canic_wasm_store_bootstrap_debug");
516 eprintln!("Diagnostic: dfx canister call {root_canister} canic_wasm_store_overview");
517 print_raw_call(root_canister, "canic_wasm_store_overview");
518 eprintln!("Diagnostic: dfx canister call {root_canister} canic_log");
519 print_recent_root_logs(root_canister);
520 return Err(format!(
521 "root bootstrap failed during phase '{}' : {}",
522 status.phase, last_error
523 )
524 .into());
525 }
526
527 let elapsed = start.elapsed().as_secs();
528 if elapsed >= timeout_seconds {
529 eprintln!("root did not report canic_ready within {timeout_seconds}s");
530 eprintln!("Diagnostic: dfx canister call {root_canister} canic_bootstrap_status");
531 print_raw_call(root_canister, protocol::CANIC_BOOTSTRAP_STATUS);
532 eprintln!("Diagnostic: dfx canister call {root_canister} canic_subnet_registry");
533 print_raw_call(root_canister, "canic_subnet_registry");
534 eprintln!(
535 "Diagnostic: dfx canister call {root_canister} canic_wasm_store_bootstrap_debug"
536 );
537 print_raw_call(root_canister, "canic_wasm_store_bootstrap_debug");
538 eprintln!("Diagnostic: dfx canister call {root_canister} canic_wasm_store_overview");
539 print_raw_call(root_canister, "canic_wasm_store_overview");
540 eprintln!("Diagnostic: dfx canister call {root_canister} canic_log");
541 print_recent_root_logs(root_canister);
542 return Err("root did not become ready".into());
543 }
544
545 if elapsed >= next_report {
546 println!("Still waiting for {root_canister} canic_ready ({elapsed}s elapsed)");
547 if let Some(status) = root_bootstrap_status(root_canister)? {
548 match status.last_error.as_deref() {
549 Some(last_error) => println!(
550 "Current bootstrap status: phase={} ready={} error={}",
551 status.phase, status.ready, last_error
552 ),
553 None => println!(
554 "Current bootstrap status: phase={} ready={}",
555 status.phase, status.ready
556 ),
557 }
558 }
559 if let Ok(registry_json) =
560 dfx_call(root_canister, "canic_subnet_registry", None, Some("json"))
561 {
562 println!("Current subnet registry roles:");
563 println!(" {}", registry_roles(®istry_json));
564 }
565 next_report = elapsed + 5;
566 }
567
568 thread::sleep(Duration::from_secs(1));
569 }
570}
571
572fn root_ready(root_canister: &str) -> Result<bool, Box<dyn std::error::Error>> {
574 let output = dfx_call(root_canister, "canic_ready", None, Some("json"))?;
575 let data = serde_json::from_str::<Value>(&output)?;
576 Ok(parse_root_ready_value(&data))
577}
578
579fn root_bootstrap_status(
581 root_canister: &str,
582) -> Result<Option<BootstrapStatusSnapshot>, Box<dyn std::error::Error>> {
583 let output = match dfx_call(
584 root_canister,
585 protocol::CANIC_BOOTSTRAP_STATUS,
586 None,
587 Some("json"),
588 ) {
589 Ok(output) => output,
590 Err(err) => {
591 let message = err.to_string();
592 if message.contains("has no query method")
593 || message.contains("method not found")
594 || message.contains("Canister has no query method")
595 {
596 return Ok(None);
597 }
598 return Err(err);
599 }
600 };
601 let data = serde_json::from_str::<Value>(&output)?;
602 Ok(parse_bootstrap_status_value(&data))
603}
604
605fn parse_root_ready_value(data: &Value) -> bool {
607 matches!(data, Value::Bool(true)) || matches!(data.get("Ok"), Some(Value::Bool(true)))
608}
609
610fn parse_bootstrap_status_value(data: &Value) -> Option<BootstrapStatusSnapshot> {
611 serde_json::from_value::<BootstrapStatusSnapshot>(data.clone())
612 .ok()
613 .or_else(|| {
614 data.get("Ok")
615 .cloned()
616 .and_then(|ok| serde_json::from_value::<BootstrapStatusSnapshot>(ok).ok())
617 })
618}
619
620fn print_install_timing_summary(timings: &InstallTimingSummary, total: Duration) {
621 println!("Install timing summary:");
622 println!("{:<20} {:>10}", "phase", "elapsed");
623 println!("{:<20} {:>10}", "--------------------", "----------");
624 print_timing_row("create_canisters", timings.create_canisters);
625 print_timing_row("build_all", timings.build_all);
626 print_timing_row("emit_manifest", timings.emit_manifest);
627 print_timing_row("fabricate_cycles", timings.fabricate_cycles);
628 print_timing_row("install_root", timings.install_root);
629 print_timing_row("stage_release_set", timings.stage_release_set);
630 print_timing_row("resume_bootstrap", timings.resume_bootstrap);
631 print_timing_row("wait_ready", timings.wait_ready);
632 print_timing_row("total", total);
633}
634
635fn print_timing_row(label: &str, duration: Duration) {
636 println!("{label:<20} {:>9.2}s", duration.as_secs_f64());
637}
638
639fn print_install_result_summary(network: &str, fleet: &str, state_path: &Path) {
641 println!("Install result:");
642 println!("{:<14} success", "status");
643 println!("{:<14} {}", "fleet", fleet);
644 println!("{:<14} {}", "install_state", state_path.display());
645 println!("{:<14} canic list --network {}", "smoke_check", network);
646}
647
648fn print_recent_root_logs(root_canister: &str) {
650 let page_args = r"(null, null, null, record { limit = 8; offset = 0 })";
651 let Ok(logs_json) = dfx_call(root_canister, "canic_log", Some(page_args), Some("json")) else {
652 return;
653 };
654 let Ok(data) = serde_json::from_str::<Value>(&logs_json) else {
655 return;
656 };
657 let entries = data
658 .get("Ok")
659 .and_then(|ok| ok.get("entries"))
660 .and_then(Value::as_array)
661 .cloned()
662 .unwrap_or_default();
663
664 if entries.is_empty() {
665 println!(" <no runtime log entries>");
666 return;
667 }
668
669 for entry in entries.iter().rev() {
670 let level = entry.get("level").and_then(Value::as_str).unwrap_or("Info");
671 let topic = entry.get("topic").and_then(Value::as_str).unwrap_or("");
672 let message = entry
673 .get("message")
674 .and_then(Value::as_str)
675 .unwrap_or("")
676 .replace('\n', "\\n");
677 let topic_prefix = if topic.is_empty() {
678 String::new()
679 } else {
680 format!("[{topic}] ")
681 };
682 println!(" {level} {topic_prefix}{message}");
683 }
684}
685
686fn registry_roles(registry_json: &str) -> String {
688 serde_json::from_str::<Value>(registry_json)
689 .ok()
690 .and_then(|data| {
691 data.get("Ok").and_then(Value::as_array).map(|entries| {
692 entries
693 .iter()
694 .filter_map(|entry| {
695 entry
696 .get("role")
697 .and_then(Value::as_str)
698 .map(str::to_string)
699 })
700 .collect::<Vec<_>>()
701 })
702 })
703 .map_or_else(
704 || "<unavailable>".to_string(),
705 |roles| {
706 if roles.is_empty() {
707 "<empty>".to_string()
708 } else {
709 roles.join(", ")
710 }
711 },
712 )
713}
714
715fn run_command(command: &mut Command) -> Result<(), Box<dyn std::error::Error>> {
717 dfx::run_status(command).map_err(Into::into)
718}
719
720fn run_command_stdout(command: &mut Command) -> Result<String, Box<dyn std::error::Error>> {
722 dfx::run_output(command).map_err(Into::into)
723}
724
725fn run_command_allow_failure(
727 command: &mut Command,
728) -> Result<std::process::ExitStatus, Box<dyn std::error::Error>> {
729 Ok(command.status()?)
730}
731
732fn print_raw_call(root_canister: &str, method: &str) {
734 let mut command = Command::new("dfx");
735 if let Ok(root) = dfx_root() {
736 command.current_dir(root);
737 }
738 let _ = command
739 .args(["canister", "call", root_canister, method])
740 .status();
741}