junobuild_shared/mgmt/
cmc.rs

1use crate::constants_shared::{IC_TRANSACTION_FEE_ICP, MEMO_CANISTER_TOP_UP};
2use crate::env::CMC;
3use crate::ledger::icp::transfer_payment;
4use crate::mgmt::ic::install_code;
5use crate::mgmt::settings::{create_canister_cycles, create_canister_settings};
6use crate::mgmt::types::cmc::{
7    CreateCanister, CreateCanisterResult, Cycles, NotifyError, SubnetId, SubnetSelection,
8    TopUpCanisterArgs,
9};
10use crate::mgmt::types::ic::WasmArg;
11use candid::Principal;
12use ic_cdk::api::call::{call_with_payment128, CallResult};
13use ic_cdk::api::management_canister::main::{CanisterId, CanisterInstallMode};
14use ic_cdk::call;
15use ic_ledger_types::{Subaccount, Tokens};
16
17pub async fn top_up_canister(canister_id: &CanisterId, amount: &Tokens) -> Result<(), String> {
18    // We need to hold back 1 transaction fee for the 'send' and also 1 for the 'notify'
19    let send_amount = Tokens::from_e8s(amount.e8s() - (2 * IC_TRANSACTION_FEE_ICP.e8s()));
20
21    let cmc = Principal::from_text(CMC).unwrap();
22
23    let to_sub_account: Subaccount = convert_principal_to_sub_account(canister_id.as_slice());
24
25    let block_index = transfer_payment(
26        &cmc,
27        &to_sub_account,
28        MEMO_CANISTER_TOP_UP,
29        send_amount,
30        IC_TRANSACTION_FEE_ICP,
31    )
32    .await
33    .map_err(|e| format!("failed to call ledger: {:?}", e))?
34    .map_err(|e| format!("ledger transfer error {:?}", e))?;
35
36    let args = TopUpCanisterArgs {
37        block_index,
38        canister_id: *canister_id,
39    };
40
41    let result: CallResult<(Result<Cycles, NotifyError>,)> =
42        call(cmc, "notify_top_up", (args,)).await;
43
44    match result {
45        Err((_, message)) => {
46            // If the topup fails in the Cmc canister, it refunds the caller.
47            // let was_refunded = matches!(error, NotifyError::Refunded { .. });
48            Err(["Top-up failed.", &message].join(" - "))
49        }
50        Ok(_) => Ok(()),
51    }
52}
53
54fn convert_principal_to_sub_account(principal_id: &[u8]) -> Subaccount {
55    let mut bytes = [0u8; 32];
56    bytes[0] = principal_id.len().try_into().unwrap();
57    bytes[1..1 + principal_id.len()].copy_from_slice(principal_id);
58    Subaccount(bytes)
59}
60
61/// Asynchronously creates a new canister and installs the provided Wasm code with additional cycles.
62///
63/// # Arguments
64/// - `controllers`: A list of `Principal` IDs to set as controllers of the new canister.
65/// - `wasm_arg`: Wasm binary and arguments to install in the new canister (`WasmArg` struct).
66/// - `cycles`: Additional cycles to deposit during canister creation on top of `CREATE_CANISTER_CYCLES`.
67/// - `subnet_id`: The `SubnetId` where the canister should be created.
68///
69/// # Returns
70/// - `Ok(Principal)`: On success, returns the `Principal` ID of the newly created canister.
71/// - `Err(String)`: On failure, returns an error message.
72pub async fn cmc_create_canister_install_code(
73    controllers: Vec<Principal>,
74    wasm_arg: &WasmArg,
75    cycles: u128,
76    subnet_id: &SubnetId,
77) -> Result<Principal, String> {
78    let cmc = Principal::from_text(CMC).unwrap();
79
80    let create_canister_arg = CreateCanister {
81        subnet_type: None,
82        subnet_selection: Some(SubnetSelection::Subnet { subnet: *subnet_id }),
83        settings: create_canister_settings(controllers),
84    };
85
86    let result: CallResult<(CreateCanisterResult,)> = call_with_payment128(
87        cmc,
88        "create_canister",
89        (create_canister_arg,),
90        create_canister_cycles(cycles),
91    )
92    .await;
93
94    match result {
95        Err((_, message)) => Err(["Failed to call CMC to create canister.", &message].join(" - ")),
96        Ok((result,)) => match result {
97            Err(err) => Err(format!("Failed to create canister with CMC - {}", err)),
98            Ok(canister_id) => {
99                let install =
100                    install_code(canister_id, wasm_arg, CanisterInstallMode::Install).await;
101
102                match install {
103                    Err(_) => {
104                        Err("Failed to install code in canister created with CMC.".to_string())
105                    }
106                    Ok(_) => Ok(canister_id),
107                }
108            }
109        },
110    }
111}