canic_core/interface/ic/
cycles.rs1use crate::{
2 Error,
3 cdk::{
4 api::canister_self,
5 types::{Account, Nat, Principal, Subaccount},
6 utils::time::now_nanos,
7 },
8 env::nns::{CYCLES_MINTING_CANISTER, ICP_LEDGER_CANISTER},
9 interface::ic::{call::Call, derive_subaccount},
10 spec::{
11 ic::cycles::{IcpXdrConversionRateResponse, NotifyTopUpArgs},
12 icrc::icrc1::Icrc1TransferArgs,
13 },
14};
15
16pub async fn get_icp_xdr_conversion_rate() -> Result<f64, Error> {
26 let res = Call::unbounded_wait(*CYCLES_MINTING_CANISTER, "get_icp_xdr_conversion_rate")
27 .await
28 .map_err(|e| Error::custom(format!("CMC.get_icp_xdr_conversion_rate failed: {e:?}")))?;
29
30 let rate_info: IcpXdrConversionRateResponse = res
31 .candid()
32 .map_err(|e| Error::custom(format!("decode error: {e:?}")))?;
33
34 #[allow(clippy::cast_precision_loss)]
36 let xdr_per_icp = rate_info.data.xdr_permyriad_per_icp as f64 / 10_000.0;
37
38 if xdr_per_icp <= 0.0 {
39 return Err(Error::custom(format!(
40 "invalid rate from CMC: xdr_per_icp={xdr_per_icp}"
41 )));
42 }
43
44 Ok(1.0 / xdr_per_icp)
45}
46
47pub async fn convert_icp_to_cycles(
50 icp_ledger_account: Account,
51 cycles_needed: u64,
52) -> Result<(), Error> {
53 let icp_per_xdr = get_icp_xdr_conversion_rate().await?;
57 let icp_needed_e8s = calculate_icp_for_cycles(cycles_needed, icp_per_xdr);
58
59 let caller = canister_self();
63 let subaccount = derive_subaccount(&caller, "convert_cycles");
64
65 let block_index = transfer_icp_to_cmc(icp_ledger_account, subaccount, icp_needed_e8s).await?;
69
70 notify_cycles_minting_canister(block_index, caller).await?;
74
75 Ok(())
76}
77
78#[allow(clippy::cast_possible_truncation)]
92#[allow(clippy::cast_precision_loss)]
93#[allow(clippy::cast_sign_loss)]
94fn calculate_icp_for_cycles(cycles_needed: u64, icp_per_xdr: f64) -> u64 {
95 const CYCLES_PER_XDR: f64 = 1_000_000_000_000.0;
96 const E8S_PER_ICP: f64 = 100_000_000.0;
97 const BUFFER: f64 = 1.05;
98
99 let xdr_required = cycles_needed as f64 / CYCLES_PER_XDR;
100 let icp_required = xdr_required * icp_per_xdr * BUFFER;
101
102 (icp_required * E8S_PER_ICP).round() as u64
103}
104
105async fn transfer_icp_to_cmc(
107 from_account: Account,
108 deposit_subaccount: Subaccount,
109 icp_amount_e8s: u64,
110) -> Result<u64, Error> {
111 let args = Icrc1TransferArgs {
112 from_subaccount: from_account.subaccount,
113 to: Account {
114 owner: *CYCLES_MINTING_CANISTER,
115 subaccount: Some(deposit_subaccount),
116 },
117 amount: Nat::from(icp_amount_e8s),
118 fee: None,
119 memo: Some(b"cycle_conversion".to_vec()),
120 created_at_time: Some(now_nanos()),
121 };
122
123 let raw = Call::unbounded_wait(*ICP_LEDGER_CANISTER, "icrc1_transfer")
124 .with_args(&(args,))
125 .await
126 .map_err(|e| Error::custom(format!("ICP ledger transfer failed: {e:?}")))?;
127
128 let result: Result<Nat, Nat> = raw
130 .candid()
131 .map_err(|e| Error::custom(format!("Failed to decode icrc1_transfer(): {e:?}")))?;
132
133 match result {
134 Ok(block) => block
135 .0
136 .try_into()
137 .map_err(|_| Error::custom("transfer block index does not fit into u64")),
138 Err(err_code) => Err(Error::custom(format!(
139 "ICP transfer rejected by ledger: {err_code}"
140 ))),
141 }
142}
143
144async fn notify_cycles_minting_canister(
148 block_index: u64,
149 recipient: Principal,
150) -> Result<(), Error> {
151 let args = NotifyTopUpArgs {
152 block_index,
153 canister_id: recipient,
154 };
155
156 let raw = Call::unbounded_wait(*CYCLES_MINTING_CANISTER, "notify_top_up")
157 .with_args(&(args,))
158 .await
159 .map_err(|e| Error::custom(format!("CMC.notify_top_up transport failure: {e:?}")))?;
160
161 let method_result: Result<(), Error> = raw
162 .candid()
163 .map_err(|e| Error::custom(format!("decode failure in notify_top_up: {e:?}")))?;
164
165 method_result.map_err(|e| Error::custom(format!("CMC rejected notify_top_up: {e:?}")))
166}
167
168#[cfg(test)]
175mod tests {
176 use super::calculate_icp_for_cycles;
177
178 #[test]
179 fn calculates_icp_for_cycles_with_buffer() {
180 let cycles_needed = 2_000_000_000_000u64; let icp_per_xdr = 1.25_f64; let icp_e8s = calculate_icp_for_cycles(cycles_needed, icp_per_xdr);
183 assert_eq!(icp_e8s, 262_500_000); }
185}