Programmatically mint cycles in rust

I realized there is no Rust library for converting ICP tokens into cycles. The normal way would be to use DFX. In a situation where, for example, you want to deploy canisters for your users programmatically, you need cycles for those canisters, and having a way to mint them programmatically would be useful. I was trying to implement this functionality myself and there were no tools existed to do this but I found this project ,multisig_index/src at master · Catalyze-Software/multisig_index · GitHub, was implementing this and pieced their code together into this library, https://github.com/amschel99/mint-cycles

All credits to them

3 Likes

Hello there @amschel99
I wanted to integrate this rust library that you made in my project and when I went through the source code, I see that in the function: transfer_icp, you’ve used this function: transfer
calling the icp ledger, ofcourse. What my question is, if I’d like to take ICP from the user, and then use that to convert to cycles for a new canister creation or top up. We usually use, approve and transfer_from functions of icrc2 in icp ledger, So, my question is in order to use your library in my project, will I have to alter the transfer function or just proceed with this one? Please let me know, thanks.

It’s been a while since I logged in here. Hope you got the help you wanted. In my setup, what I was doing is make the user send some icp to the backend canister that I want to mint cycles for using Plug wallet. They have logic for this which uses icrc2 approve. When the canister has ICP i now use this library to mint cycles. You can also write the function to spend on behalf of the user too using icrc2 approve.

1 Like

Nice to see that the code I’ve written is being used, a reference to it would have been nice tho.

1 Like

All credits to you. Powerful code

1 Like

@rem.codes @amschel99
That’s exactly what I want to do as well.
I want to take ICP from user and have it send to my backend canister, but we always need their approval before the transfer right? icrc2_approve → icrc2_transfer_from.
My question was, in order to do exactly this does the function need any alteration?
It’ll be great help, if any of you can help me with this. Thank you!

There are a couple of flows you can follow, the main approach i usually take is adding cycles to the “parent” canister and use that to spin up a “child” canister which is the flow explained below.

You can also spin up a canister directly from the CMC canister by using the right memo and skip a call, but i was running into issues with pocket-ic that it tried to create the new canister on the NNS subnet.

You should send ICP to the “parent canister” and then following the flow below, optionally using a subaccount (left it out to make it easier to follow)


Ledger
Structs used are coming from the following package

ic-ledger-types = "0.13"
pub static MEMO_TOP_UP_CANISTER: Memo = Memo(1347768404);
pub static MEMO_CREATE_CANISTER: Memo = Memo(1095062083);

pub async fn topup_self(
    icp_amount_e8s: u64,
    canister: Principal,
) -> CanisterResult<Nat> {
    let amount = Tokens::from_e8s(icp_amount_e8s) - ICP_TRANSACTION_FEE;

    let args = TransferArgs {
        memo: MEMO_TOP_UP_CANISTER, // important
        amount,
        fee: ICP_TRANSACTION_FEE,
        from_subaccount: None,
        to: AccountIdentifier::new(
            &MAINNET_CYCLES_MINTING_CANISTER_ID, // imported from ic-ledger-types
            &Subaccount::from(canister), // important
        ),
        created_at_time: None
    };

    match transfer(MAINNET_LEDGER_CANISTER_ID, args).await {
        Ok(result) => match result {
            Ok(block_index) => notify_top_up_cycles(block_index).await,
            Err(err) => todo!("handle error"),
        },
        Err(err) => todo!("handle error"),
    }
}


CMC
The CyclesMintingService can be pulled from the rust declarations on the bottom of the linked dashboard page, this holds the methods and structs needed for the method below.

pub async fn notify_top_up_cycles(block_index: u64) -> CanisterResult<Nat> {
    match CyclesMintingService(MAINNET_CYCLES_MINTING_CANISTER_ID)
        .notify_top_up(NotifyTopUpArg {
            block_index, // block index of the transaction
            canister_id: id(), // principal set in the transfer.to subaccount
        })
        .await
    {
        Ok((result,)) => match result {
            NotifyTopUpResult::Ok(cycles) => Ok(cycles),
            NotifyTopUpResult::Err(err) => todo!("handle error"),
        },
        Err((_, err)) => todo!("handle error"),
    }
}


Create canister
Finally creating the canister

ic-cdk = "0.16"
pub async fn deploy_canister(
    cycles: u64,
    controllers: Vec<Principal>,
) -> CanisterResult<Principal> {
    let args = CreateCanisterArgument { 
        settings: Some(CanisterSettings {
            controllers: Some(controllers),
            compute_allocation: None,
            memory_allocation: None,
            freezing_threshold: None,
            reserved_cycles_limit: None,
            wasm_memory_limit: None,
            log_visibility: None,
        }),
    };

    create_canister(args, cycles as u128)
        .await
        .map(|(result,)| result.canister_id)
        .map_err(|(_, err)| todo!("handle error"))
}

also some helper functions that are used for converting the cycles to the right format.

pub fn nat_to_f64(n: &Nat) -> f64 {
    let n_str = n.0.to_string();
    n_str.parse::<f64>().unwrap()
}

pub fn f64_to_u64(f: f64) -> u64 {
    f.round() as u64
}

pub fn nat_to_u64(n: &Nat) -> u64 {
    f64_to_u64(nat_to_f64(n))
}

so in the end it would look something like

let cycles = topup_self(icp_e8s, id()).await?;
let new_canister = deploy_canister(nat_to_u64(&cycles), vec![id()]).await?;
1 Like