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,
7};
8use crate::ic::DecodeCandid;
9use crate::ledger::icp::transfer_payment;
10use crate::mgmt::ic::install_code;
11use crate::mgmt::settings::{create_canister_cycles, create_canister_settings};
12use crate::mgmt::types::cmc::{
13    CreateCanister, CreateCanisterResult, Cycles, NotifyError, SubnetId, SubnetSelection,
14    TopUpCanisterArgs,
15};
16use crate::mgmt::types::ic::{CreateCanisterInitSettingsArg, WasmArg};
17use candid::Principal;
18use ic_cdk::call::Call;
19use ic_cdk::management_canister::{CanisterId, CanisterInstallMode};
20use ic_ledger_types::{Subaccount, Tokens};
21
22/// Tops up a canister's cycles balance by transferring ICP to the Cycles Minting Canister (CMC).
23///
24/// This function performs a two-step process:
25/// 1. Transfers ICP tokens to the CMC's subaccount for the target canister
26/// 2. Notifies the CMC to convert the ICP into cycles and credit the canister
27///
28/// The function automatically deducts two transaction fees from the amount (one for the transfer,
29/// one for the notification). If the notification fails, the CMC automatically refunds the caller.
30///
31/// # Arguments
32/// - `canister_id`: The ID of the canister to top up
33/// - `amount`: The total ICP amount including transaction fees (minimum: 2 * IC_TRANSACTION_FEE_ICP)
34///
35/// # Returns
36/// - `Ok(())`: On success, the canister has been topped up with cycles
37/// - `Err(String)`: On failure, returns an error message describing what went wrong
38///
39/// # Errors
40/// - Ledger transfer failures (insufficient balance, invalid recipient, etc.)
41/// - CMC notification failures (though CMC will refund in these cases)
42pub async fn top_up_canister(canister_id: &CanisterId, amount: &Tokens) -> Result<(), String> {
43    // We need to hold back 1 transaction fee for the 'send' and also 1 for the 'notify'
44    let send_amount = Tokens::from_e8s(amount.e8s() - (2 * IC_TRANSACTION_FEE_ICP.e8s()));
45
46    let cmc = Principal::from_text(CMC).unwrap();
47
48    let to_sub_account: Subaccount = convert_principal_to_sub_account(canister_id.as_slice());
49
50    let block_index = transfer_payment(
51        &cmc,
52        &to_sub_account,
53        MEMO_CANISTER_TOP_UP,
54        send_amount,
55        IC_TRANSACTION_FEE_ICP,
56    )
57    .await
58    .map_err(|e| format!("{JUNO_ERROR_CMC_CALL_LEDGER_FAILED} ({e:?})"))?
59    .map_err(|e| format!("{JUNO_ERROR_CMC_LEDGER_TRANSFER_FAILED} ({e:?})"))?;
60
61    let args = TopUpCanisterArgs {
62        block_index,
63        canister_id: *canister_id,
64    };
65
66    // If the topup fails in the Cmc canister, it refunds the caller.
67    // let was_refunded = matches!(error, NotifyError::Refunded { .. });
68    let _ = Call::unbounded_wait(cmc, "notify_top_up")
69        .with_arg(args)
70        .await
71        .decode_candid::<Result<Cycles, NotifyError>>()?;
72
73    Ok(())
74}
75
76fn convert_principal_to_sub_account(principal_id: &[u8]) -> Subaccount {
77    let mut bytes = [0u8; 32];
78    bytes[0] = principal_id.len().try_into().unwrap();
79    bytes[1..1 + principal_id.len()].copy_from_slice(principal_id);
80    Subaccount(bytes)
81}
82
83/// Asynchronously creates a new canister and installs the provided Wasm code with additional cycles.
84///
85/// # Arguments
86/// - `create_settings_arg`: The custom settings to apply to spinup the canister.
87/// - `wasm_arg`: Wasm binary and arguments to install in the new canister (`WasmArg` struct).
88/// - `cycles`: Additional cycles to deposit during canister creation on top of `CREATE_CANISTER_CYCLES`.
89/// - `subnet_id`: The `SubnetId` where the canister should be created.
90///
91/// # Returns
92/// - `Ok(Principal)`: On success, returns the `Principal` ID of the newly created canister.
93/// - `Err(String)`: On failure, returns an error message.
94pub async fn create_and_install_canister_with_cmc(
95    create_settings_arg: &CreateCanisterInitSettingsArg,
96    wasm_arg: &WasmArg,
97    cycles: u128,
98    subnet_id: &SubnetId,
99) -> Result<Principal, String> {
100    let canister_id = create_canister_with_cmc(create_settings_arg, cycles, subnet_id).await?;
101
102    install_code(canister_id, wasm_arg, CanisterInstallMode::Install)
103        .await
104        .map_err(|_| JUNO_ERROR_CMC_INSTALL_CODE_FAILED.to_string())?;
105
106    Ok(canister_id)
107}
108
109/// Creates a new canister on a specific subnet using the Cycles Minting Canister (CMC).
110///
111/// # Arguments
112/// - `create_settings_arg`: Initial settings for the canister (controllers, compute allocation, etc.)
113/// - `cycles`: The number of cycles to deposit into the new canister
114/// - `subnet_id`: The ID of the subnet where the canister should be created
115///
116/// # Returns
117/// - `Ok(Principal)`: On success, returns the Principal ID of the newly created canister
118/// - `Err(String)`: On failure, returns an error message describing what went wrong
119///
120/// # Errors
121/// - CMC call failures (network issues, invalid arguments, etc.)
122/// - CMC canister creation failures (insufficient cycles, subnet unavailable, etc.)
123pub async fn create_canister_with_cmc(
124    create_settings_arg: &CreateCanisterInitSettingsArg,
125    cycles: u128,
126    subnet_id: &SubnetId,
127) -> Result<Principal, String> {
128    let cmc = Principal::from_text(CMC).unwrap();
129
130    let create_canister_arg = CreateCanister {
131        subnet_type: None,
132        subnet_selection: Some(SubnetSelection::Subnet { subnet: *subnet_id }),
133        settings: create_canister_settings(create_settings_arg),
134    };
135
136    let result = Call::unbounded_wait(cmc, "create_canister")
137        .with_arg(create_canister_arg)
138        .with_cycles(create_canister_cycles(cycles))
139        .await
140        .decode_candid::<CreateCanisterResult>();
141
142    result
143        .map_err(|error| {
144            format!(
145                "{} ({})",
146                JUNO_ERROR_CMC_CALL_CREATE_CANISTER_FAILED, &error
147            )
148        })?
149        .map_err(|err| format!("{JUNO_ERROR_CMC_CREATE_CANISTER_FAILED} ({err})"))
150}