How to programmatically burn ICP into cycles

I’m trying to programmatically convert ICP tokens into cycles in order to build canisters programmatically for my users. However, I can’t seem to find any rust code to do this. So far what I have is this but it doesn’t work still.

pub async fn mint_cycles(amount: Tokens) -> Result<candid::Nat, std::string::String> {
    ic_cdk::print(&MAINNET_CYCLES_MINTING_CANISTER_ID.to_string());
    let transfer_args = TransferArgs {
        memo: MEMO_TOP_UP_CANISTER,
        amount,
        fee: ICP_TRANSACTION_FEE,
        from_subaccount: Some(Subaccount::from(
            Principal::from_text("owu57-ix3tx-4pgh7-pmu7n-dzlor-tqljq-wui5j-g5b2g-mtnfa-yklry-mae")
                .unwrap(),
        )),
        to: AccountIdentifier::new(
            &Principal::from_text("rkp4c-7iaaa-aaaaa-aaaca-cai").unwrap(),
            &Subaccount::from(id()),
        ),
        created_at_time: None,
    };

    match Ledger::transfer_icp(transfer_args).await {
        // If the transaction is successfull, return the block index of the transaction
        Ok(cmc_block_index) => match CMC::top_up_self(cmc_block_index).await {
            Ok(cycles) => Ok(cycles),
            Err(err) => Err(err),
        },
        Err(err) => Err(err.to_string()),
    }
}

The Ledger struct is

use candid::Principal;
use ic_cdk::id;
use ic_ledger_types::{
    query_archived_blocks, query_blocks, transfer, AccountIdentifier, Block, BlockIndex,
    GetBlocksArgs, Tokens, TransferArgs, DEFAULT_SUBACCOUNT, MAINNET_LEDGER_CANISTER_ID,
};

pub struct Ledger {}

impl Ledger {
    pub async fn transfer_icp(args: TransferArgs) -> Result<u64, String> {
        match transfer(MAINNET_LEDGER_CANISTER_ID, args).await {
            Ok(result) => match result {
                Ok(block_index) => Ok(block_index),
                Err(err) => Err(err.to_string()),
            },
            Err((_, err)) => Err(err),
        }
    }

    // This method checks if the transaction is send and received from the given principal
    pub async fn validate_transaction(
        principal: Principal,
        block_index: BlockIndex,
    ) -> Result<Tokens, String> {
        // Get the block
        let block = Self::get_block(block_index).await;
        match block {
            Some(block) => {
                // Check if the block has a transaction
                if let Some(operation) = block.transaction.operation {
                    if let ic_ledger_types::Operation::Transfer {
                        from,
                        to,
                        amount,
                        fee: _, // Ignore fee
                    } = operation
                    {
                        if from != Self::principal_to_account_identifier(principal) {
                            return Err("Transaction not from the given principal".to_string());
                        }
                        if to != Self::principal_to_account_identifier(id()) {
                            return Err("Transaction not to the given principal".to_string());
                        }
                        return Ok(amount);
                    } else {
                        // Not a transfer
                        return Err("Not a transfer".to_string());
                    }
                } else {
                    // No operation
                    return Err("No operation".to_string());
                }
            }
            // No block
            None => return Err("No block".to_string()),
        }
    }

    async fn get_block(block_index: BlockIndex) -> Option<Block> {
        let args = GetBlocksArgs {
            start: block_index,
            length: 1,
        };

        match query_blocks(MAINNET_LEDGER_CANISTER_ID, args.clone()).await {
            Ok(blocks_result) => {
                if blocks_result.blocks.len() >= 1 {
                    debug_assert_eq!(blocks_result.first_block_index, block_index);
                    return blocks_result.blocks.into_iter().next();
                }

                if let Some(func) = blocks_result.archived_blocks.into_iter().find_map(|b| {
                    (b.start <= block_index && (block_index - b.start) < b.length)
                        .then(|| b.callback)
                }) {
                    match query_archived_blocks(&func, args).await {
                        Ok(range) => match range {
                            Ok(_range) => return _range.blocks.into_iter().next(),
                            Err(_) => return None,
                        },
                        _ => (),
                    }
                }
            }
            Err(_) => (),
        }

        None
    }

    fn principal_to_account_identifier(principal: Principal) -> AccountIdentifier {
        AccountIdentifier::new(&principal, &DEFAULT_SUBACCOUNT)
    }
}

and the code for cycles minting canister is

use candid::Nat;
use ic_cdk::id;
use ic_ledger_types::MAINNET_CYCLES_MINTING_CANISTER_ID;

use crate::rust_declarations::cmc_service::{CmcService, NotifyTopUpArg, NotifyTopUpResult};

pub struct CMC {}

impl CMC {
    pub async fn top_up_self(block_index: u64) -> Result<Nat, String> {
        match CmcService(MAINNET_CYCLES_MINTING_CANISTER_ID)
            .notify_top_up(NotifyTopUpArg {
                block_index,
                canister_id: id(),
            })
            .await
        {
            Ok((result,)) => match result {
                NotifyTopUpResult::Ok(cycles) => Ok(cycles),
                NotifyTopUpResult::Err(err) => Err(format!("{:?}", err)),
            },
            Err((_, err)) => Err(err),
        }
    }
}

I’m getting an error “Canister ryjl3-tyaaa-aaaaa-aaaba-cai not found” and that’s the id of the main ledger canister if I’m not wrong

Here is how dfx does it. Basically it transfer ICP to the CMC’s account with the user principal as the subaccount, then it calls notify_mint_cycles on the CMC. Important: use the right memo, you can get it from a few lines further up.

I suppose you are doing this locally, right? In that case you need to e.g. dfx nns install after dfx start, otherwise you don’t have a ledger or CMC canister

Thank you @Vivienne .