junobuild_shared/mgmt/
cmc.rs

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