├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── context.rs ├── guard.rs ├── lib.rs ├── memory_storage.rs ├── nat_utils.rs └── throws.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | .idea -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic_utils" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ic-cdk-macros = "0.7" 10 | candid = "0.9" 11 | ic-ledger-types = "0.7.0" 12 | ic-stable-structures = "0.5.6" 13 | ic-cdk = "0.10" 14 | ic-cdk-bindgen = "0.1.0" 15 | serde = "1.0" 16 | num-bigint = "0.4.2" 17 | #log4rs = {version= "0.13.0",features = ["console_appender"]} 18 | lazy_static = "1.4" 19 | bigdecimal = "0.4" 20 | 21 | 22 | [dev-dependencies] 23 | tokio = { version = "0.2.10", features = [ "full"] } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ICPEx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IC_Utils 2 | Designed for rapid iteration in IC Canister development projects. 3 | 4 | ## Summary 5 | `IcContext` provides a context for invoking IC canister objects, facilitating storage and retrieval of object data within the canister. 6 | 7 | `NatUtils` enables rapid generation and conversion of Nat types for natural numbers. 8 | 9 | `Guard` supports locking operations on canisters to prevent data reentry anomalies. 10 | 11 | `Throws` utility class handles exceptions, triggering panic for exceptional instances and returning data results for normal cases. 12 | 13 | ## Details 14 | ### IcContext 15 | IcContext provides the context for ic canister object invocation, which is used to store the object type and facilitate the user to store and call the object data in canister. 16 | 17 | ```rust 18 | let tll = context::get_mut::>(); 19 | ``` 20 | 21 | In this example, an empty VecDeque of type VecDeque is returned if no object of type VecDeque exists in the context, or a stored object if one exists. The returned object is actually a mutable reference, and any user edits to the reference will modify the object. 22 | 23 | 24 | ### NatUtils 25 | `nat_utils` is used for rapidly generating Nat types as natural numbers and performing mutual conversions between different types. 26 | 27 | ```rust 28 | fstr_to_nat("3.1231313",1_000_000_000_000_000_000f64); 29 | ``` 30 | In this example, we are converting numeric characters of type 'str' into Nat numbers with 18-digit precision. 31 | 32 | 33 | ### Guard 34 | The tool supports locking operations requested to canister to prevent anomalies caused by data reentry. 35 | ```rust 36 | //Add Exclusion Lock 37 | let guard = CallerGuard::new(id().clone()); 38 | if guard.is_err() { 39 | panic!("{}",guard.err().unwrap()); 40 | } 41 | ``` 42 | 43 | 44 | ### Throws 45 | The throws utility class is used to handle exceptions when called, throwing panic when the received instance is an exception and returning data results when normal. 46 | ```rust 47 | //Exception handling when creating a canister 48 | let (cid, ) = throw(create_canister(CreateCanisterArgument { 49 | settings: Some(CanisterSettings { 50 | controllers: Some(vec![id()]), 51 | compute_allocation: None, 52 | memory_allocation: None, 53 | freezing_threshold: None, 54 | }) 55 | }, 500000000000).await.map_err(|e| "Create Pool failed:".to_string() + &*e.1)); 56 | 57 | ``` 58 | 59 | ## Feature Milestones 60 | Developers continue to enhance this toolchain to support a broader range of application scenarios and to accelerate development efficiency on the IC platform. 61 | 62 | - [x] IcContext 63 | - [x] NatUtils 64 | - [x] Guard 65 | - [x] Throws 66 | - [x] LinkedHashMap:The LinkedHashMap is a custom data structure that combines the features of a hash map and a linked list. It maintains a hash map for fast key-value lookups and a linked list to preserve the order of insertion. 67 | - [ ] MemoryStorage:A method for managing large objects. 68 | - [ ] CommonCanister:A tool that integrates basic canister query interfaces such as cycles querying and memory querying services, serving as the foundational integration for any canister. 69 | 70 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | use std::collections::BTreeMap; 3 | use std::ops::{Div, Mul}; 4 | use bigdecimal::{BigDecimal, ToPrimitive}; 5 | use candid::{Nat, Principal}; 6 | use candid::utils::{ArgumentDecoder, ArgumentEncoder}; 7 | 8 | use ic_cdk; 9 | use ic_cdk::api::call::CallResult; 10 | use ic_cdk::api::management_canister::http_request::CanisterHttpRequestArgument; 11 | use num_bigint::BigUint; 12 | 13 | static mut CONTEXT: Option = None; 14 | 15 | /// A singleton context that is used in the actual IC environment. 16 | pub struct IcContext { 17 | /// The storage for this context. 18 | storage: BTreeMap>, 19 | } 20 | 21 | impl IcContext { 22 | /// Return a mutable reference to the context. 23 | #[inline(always)] 24 | pub fn context() -> &'static mut IcContext { 25 | unsafe { 26 | if let Some(ctx) = &mut CONTEXT { 27 | ctx 28 | } else { 29 | CONTEXT = Some(IcContext { 30 | storage: BTreeMap::new(), 31 | }); 32 | IcContext::context() 33 | } 34 | } 35 | } 36 | 37 | #[inline(always)] 38 | pub fn as_mut(&self) -> &mut Self { 39 | unsafe { 40 | let const_ptr = self as *const Self; 41 | let mut_ptr = const_ptr as *mut Self; 42 | &mut *mut_ptr 43 | } 44 | } 45 | #[inline(always)] 46 | pub fn trap(&self, message: &str) -> ! { 47 | ic_cdk::api::trap(message); 48 | } 49 | 50 | #[inline(always)] 51 | pub fn print>(&self, s: S) { 52 | ic_cdk::api::print(s) 53 | } 54 | 55 | #[inline(always)] 56 | pub fn id(&self) -> Principal { 57 | ic_cdk::id() 58 | } 59 | 60 | #[inline(always)] 61 | pub fn time(&self) -> u64 { 62 | ic_cdk::api::time() 63 | } 64 | 65 | #[inline(always)] 66 | pub fn balance(&self) -> u64 { 67 | ic_cdk::api::canister_balance() 68 | } 69 | 70 | #[inline(always)] 71 | pub fn caller(&self) -> Principal { 72 | ic_cdk::api::caller() 73 | } 74 | 75 | #[inline(always)] 76 | pub fn msg_cycles_available(&self) -> u64 { 77 | ic_cdk::api::call::msg_cycles_available() 78 | } 79 | 80 | #[inline(always)] 81 | pub fn msg_cycles_accept(&self, amount: u64) -> u64 { 82 | ic_cdk::api::call::msg_cycles_accept(amount) 83 | } 84 | 85 | #[inline(always)] 86 | pub fn msg_cycles_refunded(&self) -> u64 { 87 | ic_cdk::api::call::msg_cycles_refunded() 88 | } 89 | 90 | #[inline(always)] 91 | pub fn store(&self, data: T) { 92 | let type_id = TypeId::of::(); 93 | self.as_mut().storage.insert(type_id, Box::new(data)); 94 | } 95 | 96 | #[inline] 97 | pub fn get_maybe(&self) -> Option<&T> { 98 | let type_id = std::any::TypeId::of::(); 99 | self.storage 100 | .get(&type_id) 101 | .map(|b| b.downcast_ref().expect("Unexpected value of invalid type.")) 102 | } 103 | 104 | #[inline(always)] 105 | pub fn get_mut(&self) -> &mut T { 106 | let type_id = std::any::TypeId::of::(); 107 | self.as_mut() 108 | .storage 109 | .entry(type_id) 110 | .or_insert_with(|| Box::new(T::default())) 111 | .downcast_mut() 112 | .expect("Unexpected value of invalid type.") 113 | } 114 | 115 | #[inline(always)] 116 | pub fn delete(&self) -> bool { 117 | let type_id = std::any::TypeId::of::(); 118 | self.as_mut().storage.remove(&type_id).is_some() 119 | } 120 | 121 | #[inline(always)] 122 | pub fn stable_store(&self, data: T) -> Result<(), candid::Error> 123 | where 124 | T: ArgumentEncoder, 125 | { 126 | ic_cdk::storage::stable_save(data) 127 | } 128 | 129 | #[inline(always)] 130 | pub fn stable_restore(&self) -> Result 131 | where 132 | T: for<'de> ArgumentDecoder<'de>, 133 | { 134 | ic_cdk::storage::stable_restore() 135 | } 136 | 137 | 138 | #[inline(always)] 139 | pub fn set_certified_data(&self, data: &[u8]) { 140 | ic_cdk::api::set_certified_data(data); 141 | } 142 | 143 | #[inline(always)] 144 | pub fn data_certificate(&self) -> Option> { 145 | ic_cdk::api::data_certificate() 146 | } 147 | 148 | #[inline(always)] 149 | pub fn spawn>(&mut self, future: F) { 150 | ic_cdk::spawn(future) 151 | } 152 | } 153 | 154 | 155 | #[inline(always)] 156 | fn get_context() -> &'static mut IcContext { 157 | return IcContext::context(); 158 | } 159 | 160 | /// Trap the code. 161 | #[inline(always)] 162 | pub fn trap(message: &str) -> ! { 163 | get_context().trap(message) 164 | } 165 | 166 | /// Print a message. 167 | #[inline(always)] 168 | pub fn print>(s: S) { 169 | get_context().print(s) 170 | } 171 | 172 | /// ID of the current canister. 173 | #[inline(always)] 174 | pub fn id() -> Principal { 175 | get_context().id() 176 | } 177 | 178 | /// The time in nanoseconds. 179 | #[inline(always)] 180 | pub fn time() -> u64 { 181 | get_context().time() 182 | } 183 | 184 | /// The balance of the canister. 185 | #[inline(always)] 186 | pub fn balance() -> u64 { 187 | get_context().balance() 188 | } 189 | 190 | /// The caller who has invoked this method on the canister. 191 | #[inline(always)] 192 | pub fn caller() -> Principal { 193 | get_context().caller() 194 | } 195 | 196 | /// Return the number of available cycles that is sent by the caller. 197 | #[inline(always)] 198 | pub fn msg_cycles_available() -> u64 { 199 | get_context().msg_cycles_available() 200 | } 201 | 202 | /// Accept the given amount of cycles, returns the actual amount of accepted cycles. 203 | #[inline(always)] 204 | pub fn msg_cycles_accept(amount: u64) -> u64 { 205 | get_context().msg_cycles_accept(amount) 206 | } 207 | 208 | /// Return the cycles that were sent back by the canister that was just called. 209 | /// This method should only be called right after an inter-canister call. 210 | #[inline(always)] 211 | pub fn msg_cycles_refunded() -> u64 { 212 | get_context().msg_cycles_refunded() 213 | } 214 | 215 | /// Store the given data to the storage. 216 | #[inline(always)] 217 | pub fn store(data: T) { 218 | get_context().store(data) 219 | } 220 | 221 | /// Return the data that does not implement [`Default`]. 222 | #[inline(always)] 223 | pub fn get_maybe() -> Option<&'static T> { 224 | get_context().get_maybe() 225 | } 226 | 227 | /// Return the data associated with the given type. If the data is not present the default 228 | /// value of the type is returned. 229 | #[inline(always)] 230 | pub fn get() -> &'static T { 231 | get_context().get_mut() 232 | } 233 | 234 | /// Return a mutable reference to the given data type, if the data is not present the default 235 | /// value of the type is constructed and stored. The changes made to the data during updates 236 | /// is preserved. 237 | #[inline(always)] 238 | pub fn get_mut() -> &'static mut T { 239 | get_context().get_mut() 240 | } 241 | 242 | /// Remove the data associated with the given data type. 243 | #[inline(always)] 244 | pub fn delete() -> bool { 245 | get_context().delete::() 246 | } 247 | 248 | /// Store the given data to the stable storage. 249 | #[inline(always)] 250 | pub fn stable_store(data: T) -> Result<(), candid::Error> 251 | where 252 | T: ArgumentEncoder, 253 | { 254 | get_context().stable_store(data) 255 | } 256 | 257 | /// Restore the data from the stable storage. If the data is not already stored the None value 258 | /// is returned. 259 | #[inline(always)] 260 | pub fn stable_restore() -> Result 261 | where 262 | T: for<'de> ArgumentDecoder<'de>, 263 | { 264 | get_context().stable_restore() 265 | } 266 | 267 | /// Set the certified data of the canister, this method traps if data.len > 32. 268 | #[inline(always)] 269 | pub fn set_certified_data(data: &[u8]) { 270 | get_context().set_certified_data(data) 271 | } 272 | 273 | /// Returns the data certificate authenticating certified_data set by this canister. 274 | #[inline(always)] 275 | pub fn data_certificate() -> Option> { 276 | get_context().data_certificate() 277 | } 278 | 279 | /// Execute a future without blocking the current call. 280 | #[inline(always)] 281 | pub fn spawn>(future: F) { 282 | get_context().spawn(future) 283 | } 284 | 285 | 286 | pub fn nat_15() -> Nat { 287 | Nat::from(1_000_000_000_000_000u64) 288 | } 289 | 290 | pub fn nat_8() -> Nat { 291 | Nat::from(100_000_000u64) 292 | } 293 | 294 | pub fn nat_18() -> Nat { 295 | Nat::from(1_000_000_000_000_000_000u64) 296 | } 297 | 298 | pub fn nat_12() -> Nat { 299 | Nat::from(1_000_000_000_000u64) 300 | } 301 | 302 | pub fn nat_36() -> Nat { 303 | Nat::from(1_000_000_000_000_000_000_000_000_000_000_000_000u128) 304 | } 305 | 306 | pub fn require(cod: bool, e: T) -> Result<(), T> { 307 | if !cod { 308 | return Err(e); 309 | } 310 | return Ok(()); 311 | } 312 | 313 | pub fn throw(r: Result) -> V { 314 | match r { 315 | Ok(v) => { v } 316 | Err(e) => { panic!("{}", e) } 317 | } 318 | } 319 | 320 | pub fn throw_call_res(r: CallResult, msg: &str) -> V { 321 | match r { 322 | Ok(v) => { v } 323 | Err(e) => { panic!("[{}] error: {}", msg, e.1) } 324 | } 325 | } 326 | 327 | pub fn fstr_to_nat(v: String, decimal: f64) -> Nat { 328 | let l: f64 = v.parse::().unwrap() * decimal; 329 | Nat::from(l as u64) 330 | } 331 | 332 | pub fn nat_from(v: u64) -> Nat { 333 | Nat::from(v) 334 | } 335 | 336 | pub fn new_zero() -> Nat { 337 | nat_from(0) 338 | } 339 | 340 | pub fn div_to_f64(value: Nat, decimals: u8) -> f64 { 341 | let value = BigDecimal::from(nat_to_u128(value)).div(10_u64.pow(decimals as u32)).to_f64().unwrap_or(0f64); 342 | value 343 | } 344 | 345 | pub fn calc_value_to_f64(value: Nat, price: Nat, decimals: u8) -> f64 { 346 | return (BigDecimal::from(nat_to_u128(value)).div(10_u64.pow(decimals as u32))).mul( 347 | BigDecimal::from(nat_to_u128(price)).div(10_u64.pow(18u32)) 348 | ).to_f64().unwrap_or(0f64); 349 | } 350 | 351 | pub fn nat_to_u64(value: Nat) -> u64 { 352 | let v: u64 = BigUint::from(value).try_into().unwrap(); 353 | v 354 | } 355 | 356 | pub fn nat_to_u128(value: Nat) -> u128 { 357 | let v: u128 = BigUint::from(value).try_into().unwrap(); 358 | v 359 | } 360 | 361 | pub fn nat_to_u8(value: Nat) -> u8 { 362 | let v: u8 = BigUint::from(value).try_into().unwrap(); 363 | v 364 | } 365 | 366 | pub fn http_request_required_cycles(arg: &CanisterHttpRequestArgument) -> u128 { 367 | let max_response_bytes = match arg.max_response_bytes { 368 | Some(ref n) => *n as u128, 369 | None => 2 * 1024 * 1024u128, // default 2MiB 370 | }; 371 | let arg_raw = candid::utils::encode_args((arg, )).expect("Failed to encode arguments."); 372 | // The coefficients can be found in [this page](https://internetcomputer.org/docs/current/developer-docs/production/computation-and-storage-costs). 373 | // 12 is "http_request".len(). 374 | 400_000_000u128 + 100_000u128 * (arg_raw.len() as u128 + 12 + max_response_bytes) 375 | } 376 | -------------------------------------------------------------------------------- /src/guard.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::BTreeSet; 3 | use candid::{CandidType, Deserialize, Principal}; 4 | 5 | pub struct State { 6 | pending_requests: BTreeSet, 7 | } 8 | 9 | thread_local! { 10 | pub static STATE: RefCell = RefCell::new(State{pending_requests: BTreeSet::new()}); 11 | } 12 | 13 | #[derive(Deserialize, CandidType, Clone)] 14 | pub struct CallerGuard { 15 | principal: Principal, 16 | } 17 | 18 | impl CallerGuard { 19 | pub fn new(principal: Principal) -> Result { 20 | STATE.with(|state| { 21 | let pending_requests = &mut state.borrow_mut().pending_requests; 22 | if pending_requests.contains(&principal){ 23 | return Err(format!("Already processing a request for principal {:?}", &principal)); 24 | } 25 | pending_requests.insert(principal); 26 | Ok(Self { principal }) 27 | }) 28 | } 29 | 30 | pub fn unlock(principal: &Principal) { 31 | STATE.with(|state| { 32 | let flag = state.borrow_mut().pending_requests.remove(principal); 33 | }) 34 | } 35 | } 36 | 37 | impl Drop for CallerGuard { 38 | fn drop(&mut self) { 39 | STATE.with(|state| { 40 | state.borrow_mut().pending_requests.remove(&self.principal); 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod context; 2 | pub mod nat_utils; 3 | pub mod guard; 4 | pub mod throws; 5 | pub mod memory_storage; 6 | -------------------------------------------------------------------------------- /src/memory_storage.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::hash::Hash; 3 | use std::collections::{LinkedList}; 4 | 5 | // Custom implementation of a LinkedHashMap 6 | struct LinkedHashMap { 7 | map: HashMap, 8 | list: LinkedList, 9 | } 10 | 11 | 12 | impl LinkedHashMap 13 | where 14 | K: Eq + Hash + Clone, 15 | { 16 | fn new() -> Self { 17 | LinkedHashMap { 18 | map: HashMap::new(), 19 | list: LinkedList::new(), 20 | } 21 | } 22 | 23 | fn insert(&mut self, key: K, value: V) -> Option { 24 | if let Some(old_value) = self.map.insert(key.clone(), value) { 25 | return Some(old_value); 26 | } 27 | self.list.push_back(key); 28 | None 29 | } 30 | 31 | fn get(&self, key: &K) -> Option<&V> { 32 | self.map.get(key) 33 | } 34 | 35 | fn remove(&mut self, key: &K) -> Option { 36 | if let Some(value) = self.map.remove(key) { 37 | self.list = self.list.iter().cloned().filter(|k| k != key).collect(); 38 | return Some(value); 39 | } 40 | None 41 | } 42 | 43 | fn iter(&self) -> std::collections::hash_map::Iter { 44 | self.map.iter() 45 | } 46 | } 47 | // 定义一个结构体来存储对象 48 | pub struct MemoryStorage { 49 | data: LinkedHashMap>, 50 | } 51 | 52 | impl MemoryStorage { 53 | pub fn new() -> Self { 54 | MemoryStorage { 55 | data: LinkedHashMap::new(), 56 | } 57 | } 58 | 59 | pub fn write(&mut self, key: String, data: Vec) { 60 | self.data.insert(key, data); 61 | } 62 | 63 | pub fn read(&self, key: String) -> Option> { 64 | self.data.get(&key).cloned() 65 | } 66 | 67 | pub fn delete(&mut self, key: String) -> Option> { 68 | self.data.remove(&key) 69 | } 70 | 71 | pub fn list(&self) -> Vec> { 72 | let mut list = Vec::new(); 73 | for (_, value) in self.data.iter() { 74 | list.push(value.iter().cloned().collect::>()); 75 | } 76 | list 77 | } 78 | } -------------------------------------------------------------------------------- /src/nat_utils.rs: -------------------------------------------------------------------------------- 1 | use candid::Nat; 2 | use num_bigint::BigUint; 3 | 4 | pub fn fstr_to_nat(v: String, decimal:f64) ->Nat{ 5 | let l:f64= v.parse::().unwrap()*decimal; 6 | Nat::from(l as u64) 7 | } 8 | pub fn nat_from(v: u64) -> Nat { 9 | Nat::from(v) 10 | } 11 | 12 | pub fn new_zero() -> Nat { 13 | nat_from(0) 14 | } 15 | 16 | pub fn nat_to_u64(value: Nat) -> u64 { 17 | let v: u64 = BigUint::from(value).try_into().unwrap(); 18 | v 19 | } 20 | 21 | pub fn nat_8() -> Nat { 22 | Nat::from(100_000_000u64) 23 | } 24 | 25 | pub fn nat_12() -> Nat { 26 | Nat::from(1_000_000_000_000u64) 27 | } 28 | 29 | pub fn nat_15() -> Nat { 30 | Nat::from(1_000_000_000_000_000u64) 31 | } 32 | 33 | pub fn nat_18() -> Nat { 34 | Nat::from(1_000_000_000_000_000_000u64) 35 | } 36 | 37 | pub fn nat_36() -> Nat { 38 | Nat::from(1_000_000_000_000_000_000_000_000_000_000_000_000u128) 39 | } 40 | -------------------------------------------------------------------------------- /src/throws.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk::api::call::CallResult; 2 | 3 | pub fn require(cod: bool, e: T) -> Result<(), T> { 4 | if !cod { 5 | return Err(e); 6 | } 7 | return Ok(()); 8 | } 9 | 10 | pub fn throw(r: Result) -> V { 11 | match r { 12 | Ok(v) => { v } 13 | Err(e) => { panic!("{}", e) } 14 | } 15 | } 16 | 17 | pub fn throw_call_res(r: CallResult, msg: &str) -> V { 18 | match r { 19 | Ok(v) => { v } 20 | Err(e) => { panic!("[{}] error: {}", msg, e.1) } 21 | } 22 | } --------------------------------------------------------------------------------