junobuild-shared 0.8.0

Shared utilities for Juno.
Documentation
use crate::errors::{
    JUNO_ERROR_CANISTER_CREATE_FAILED, JUNO_ERROR_CANISTER_INSTALL_CODE_FAILED,
    JUNO_ERROR_CYCLES_DEPOSIT_BALANCE_LOW, JUNO_ERROR_CYCLES_DEPOSIT_FAILED,
    JUNO_ERROR_SEGMENT_DELETE_FAILED, JUNO_ERROR_SEGMENT_STOP_FAILED,
};
use crate::mgmt::settings::{create_canister_cycles, create_canister_settings};
use crate::mgmt::types::ic::{CreateCanisterInitSettingsArg, WasmArg};
use crate::types::interface::DepositCyclesArgs;
use candid::Principal;
use ic_cdk::api::canister_cycle_balance;
use ic_cdk::call::CallResult;
use ic_cdk::management_canister::{
    create_canister_with_extra_cycles, delete_canister, deposit_cycles as ic_deposit_cycles,
    install_code as ic_install_code, stop_canister, update_settings, CanisterId,
    CanisterInstallMode, CanisterSettings, CreateCanisterArgs, DeleteCanisterArgs,
    DepositCyclesArgs as MgmtDepositCyclesArgs, InstallCodeArgs, StopCanisterArgs,
    UpdateSettingsArgs,
};

/// Asynchronously creates a new canister and installs provided Wasm code with additional cycles.
///
/// # Arguments
/// - `create_settings_arg`: The custom settings to apply to spinup the canister.
/// - `wasm_arg`: Wasm binary and arguments to install in the new canister (`WasmArg` struct).
/// - `cycles`: Additional cycles to deposit during canister creation on top of `CREATE_CANISTER_CYCLES`.
///
/// # Returns
/// - `Ok(Principal)`: On success, returns the `Principal` ID of the newly created canister.
/// - `Err(String)`: On failure, returns an error message.
pub async fn create_and_install_canister_with_ic_mgmt(
    create_settings_arg: &CreateCanisterInitSettingsArg,
    wasm_arg: &WasmArg,
    cycles: u128,
) -> Result<Principal, String> {
    let canister_id = create_canister_with_ic_mgmt(create_settings_arg, cycles).await?;

    install_code(canister_id, wasm_arg, CanisterInstallMode::Install)
        .await
        .map_err(|_| JUNO_ERROR_CANISTER_INSTALL_CODE_FAILED.to_string())?;

    Ok(canister_id)
}

/// Creates a new canister using the IC management canister.
///
/// # Arguments
/// - `create_settings_arg`: Initial settings for the canister (controllers, compute allocation, etc.)
/// - `cycles`: The number of cycles to deposit into the new canister
///
/// # Returns
/// - `Ok(Principal)`: On success, returns the Principal ID of the newly created canister
/// - `Err(String)`: On failure, returns an error message describing what went wrong
///
/// # Errors
/// - Management canister call failures (network issues, invalid arguments, etc.)
/// - Canister creation failures (insufficient cycles, etc.)
pub async fn create_canister_with_ic_mgmt(
    create_settings_arg: &CreateCanisterInitSettingsArg,
    cycles: u128,
) -> Result<Principal, String> {
    let create_result = create_canister_with_extra_cycles(
        &CreateCanisterArgs {
            settings: create_canister_settings(create_settings_arg),
        },
        create_canister_cycles(cycles),
    )
    .await;

    let result = create_result.map_err(|err| {
        format!(
            "{} ({})",
            JUNO_ERROR_CANISTER_CREATE_FAILED,
            &err.to_string()
        )
    })?;

    Ok(result.canister_id)
}

/// Asynchronously installs code on a specified canister.
///
/// # Arguments
/// - `canister_id`: `Principal` ID of the target canister.
/// - `wasm_arg`: Contains the Wasm module and installation arguments.
/// - `mode`: Installation mode defined by `CanisterInstallMode`.
///
/// # Returns
/// - A `CallResult<()>` indicating success or failure.
pub async fn install_code(
    canister_id: Principal,
    WasmArg { wasm, install_arg }: &WasmArg,
    mode: CanisterInstallMode,
) -> CallResult<()> {
    let arg = InstallCodeArgs {
        mode,
        canister_id,
        wasm_module: wasm.clone(),
        arg: install_arg.clone(),
    };

    ic_install_code(&arg).await
}

/// Asynchronously updates the controller list of a specified canister.
///
/// # Arguments
/// - `canister_id`: `Principal` ID of the target canister.
/// - `controllers`: New list of `Principal` IDs to set as controllers.
///
/// # Returns
/// - A `CallResult<()>` indicating success or failure.
pub async fn update_canister_controllers(
    canister_id: Principal,
    controllers: Vec<Principal>,
) -> CallResult<()> {
    // Not including a setting in the settings record means not changing that field.
    // In other words, setting wasm_memory_limit to None here means keeping the actual value of wasm_memory_limit.
    let arg = UpdateSettingsArgs {
        canister_id,
        settings: CanisterSettings {
            controllers: Some(controllers),
            compute_allocation: None,
            memory_allocation: None,
            freezing_threshold: None,
            reserved_cycles_limit: None,
            log_visibility: None,
            wasm_memory_limit: None,
            wasm_memory_threshold: None,
            environment_variables: None,
        },
    };

    update_settings(&arg).await
}

/// Deposits cycles into a specified canister from the calling canister's balance.
///
/// # Arguments
/// - `args`: `DepositCyclesArgs` struct containing the destination canister ID and cycle amount.
///
/// # Returns
/// - `Ok(())`: On successful deposit.
/// - `Err(String)`: If the balance is insufficient or on failure to deposit.
pub async fn deposit_cycles(
    DepositCyclesArgs {
        destination_id,
        cycles,
    }: DepositCyclesArgs,
) -> Result<(), String> {
    let balance = canister_cycle_balance();

    if balance < cycles {
        return Err(format!(
            "{JUNO_ERROR_CYCLES_DEPOSIT_BALANCE_LOW} (balance {balance}, {cycles} to deposit)"
        ));
    }

    let result = ic_deposit_cycles(
        &MgmtDepositCyclesArgs {
            canister_id: destination_id,
        },
        cycles,
    )
    .await;

    match result {
        Err(err) => Err(format!(
            "{} ({})",
            JUNO_ERROR_CYCLES_DEPOSIT_FAILED,
            &err.to_string()
        )),
        Ok(_) => Ok(()),
    }
}

/// Stops the execution of a specified segment (canister).
///
/// # Arguments
/// - `canister_id`: The `CanisterId` of the canister to stop.
///
/// # Returns
/// - `Ok(())`: If the canister is successfully stopped.
/// - `Err(String)`: On failure, returns an error message.
pub async fn stop_segment(canister_id: CanisterId) -> Result<(), String> {
    let result = stop_canister(&StopCanisterArgs { canister_id }).await;

    match result {
        Err(err) => Err(format!(
            "{} ({})",
            JUNO_ERROR_SEGMENT_STOP_FAILED,
            &err.to_string()
        )),
        Ok(_) => Ok(()),
    }
}

/// Deletes a specified segment (canister).
///
/// # Arguments
/// - `canister_id`: The `CanisterId` of the canister to delete.
///
/// # Returns
/// - `Ok(())`: If the canister is successfully deleted.
/// - `Err(String)`: On failure, returns an error message.
pub async fn delete_segment(canister_id: CanisterId) -> Result<(), String> {
    let result = delete_canister(&DeleteCanisterArgs { canister_id }).await;

    match result {
        Err(err) => Err(format!(
            "{} ({})",
            JUNO_ERROR_SEGMENT_DELETE_FAILED,
            &err.to_string()
        )),
        Ok(_) => Ok(()),
    }
}