isp_sdk/isp/
mod.rs

1mod isp_did;
2use candid::{CandidType, Decode, Encode, Nat};
3use garcon::Delay;
4use hex::{self};
5use ic_agent::agent::http_transport::ReqwestHttpReplicaV2Transport;
6use ic_agent::{identity::Secp256k1Identity, Agent};
7pub use isp_did::{CreateICSPResult, Error, TopUpArgs, TopUpResult, TransferResult};
8use serde::Deserialize;
9
10static ISP_CANISTER_ID_TEXT: &'static str = "p2pki-xyaaa-aaaan-qatua-cai";
11
12/// Get icsps of user, return Vec<(icsp_name, icsp_canister_id)>
13///
14/// # Examples
15///
16/// ``` no_run
17/// use isp_sdk::isp;
18///
19/// pub async fn get_user_icsps() {
20///     let response = isp::get_user_icsps("identities/identity.pem").await;
21///     for i in &response {
22///         println!("icsp_name:{:?},icsp_canister_id:{:?}", i.0, i.1.to_text());
23///     }
24///     if response.is_empty() {
25///         println!("user do not have icsp\n");
26///     }
27/// }
28/// ```
29pub async fn get_user_icsps(pem_identity_path: &str) -> Vec<(String, candid::Principal)> {
30    let canister_id = candid::Principal::from_text(ISP_CANISTER_ID_TEXT).unwrap();
31    let response_blob = build_agent(pem_identity_path)
32        .query(&canister_id, "getUserICSPs")
33        .with_arg(Encode!().expect("encode error"))
34        .call()
35        .await
36        .expect("response error");
37    let response = Decode!(&response_blob, Vec<(String, candid::Principal)>).unwrap();
38    response
39}
40
41/// Get user's subAccount of the isp
42///
43/// You should transfer icp to this subAccount in order to create icsp canister
44///
45/// # Examples
46///
47/// ``` no_run
48/// use isp_sdk::isp;
49///
50/// pub async fn get_sub_account() {
51///     println!(
52///         "SubAccount:{:?}\n",
53///         isp::get_sub_account("identities/identity.pem").await
54///     );
55/// }
56/// ```
57pub async fn get_sub_account(pem_identity_path: &str) -> String {
58    let canister_id = candid::Principal::from_text(ISP_CANISTER_ID_TEXT).unwrap();
59    let response_blob = build_agent(pem_identity_path)
60        .query(&canister_id, "getSubAccount")
61        .with_arg(Encode!().expect("encode error"))
62        .call()
63        .await
64        .expect("response error");
65    let response = Decode!(&response_blob, Vec<u8>).unwrap();
66    hex::encode(response)
67}
68
69/// Get the icp balance of user's subAccount of the isp
70///
71/// The balance is e8s
72///
73/// # Examples
74///
75/// ``` no_run
76/// use isp_sdk::isp;
77///
78/// pub async fn get_user_sub_account_icp_balance() {
79///     println!(
80///         "icp balance:{:?}\n",
81///         isp::get_user_sub_account_icp_balance("identities/identity.pem").await
82///     );
83/// }
84/// ```
85pub async fn get_user_sub_account_icp_balance(pem_identity_path: &str) -> u64 {
86    let canister_id = candid::Principal::from_text(ISP_CANISTER_ID_TEXT).unwrap();
87    let response_blob = build_agent(pem_identity_path)
88        .update(&canister_id, "getUserSubAccountICPBalance")
89        .with_arg(Encode!().expect("encode error"))
90        .call_and_wait()
91        .await
92        .expect("response error");
93    let response = Decode!(&response_blob, u64).unwrap();
94    response
95}
96
97/// Transfer out icp from user's subAccount of the isp
98///
99/// The amount is e8s
100///
101/// # Examples
102///
103/// ``` no_run
104/// use isp_sdk::isp;
105///
106/// pub async fn transfer_out_user_sub_account_icp() {
107///     println!(
108///         "transfer out icp result:{:?}\n",
109///         isp::transfer_out_user_sub_account_icp(
110///             "identities/identity.pem",
111///             "3eee9b4671b8fde5a501288d74d21ee93042dc202104fa35051563ae35d24f2f",
112///             5000000 as u64
113///         )
114///         .await
115///     );
116/// }
117/// ```
118pub async fn transfer_out_user_sub_account_icp(
119    pem_identity_path: &str,
120    to: &str,
121    amount: u64,
122) -> TransferResult {
123    let canister_id = candid::Principal::from_text(ISP_CANISTER_ID_TEXT).unwrap();
124    let response_blob = build_agent(pem_identity_path)
125        .update(&canister_id, "transferOutUserSubAccountICP")
126        .with_arg(Encode!(&(hex::decode(to).unwrap()), &amount).expect("encode error"))
127        .call_and_wait()
128        .await
129        .expect("response error");
130    let response = Decode!(&response_blob, TransferResult).unwrap();
131    response
132}
133
134/// Get admins of isp
135///
136/// # Examples
137///
138/// ``` no_run
139/// use candid::Principal;
140/// use isp_sdk::isp;
141///
142/// pub async fn get_isp_admins() {
143///     println!("isp admins:");
144///     for i in &isp::get_isp_admins("identities/identity.pem").await {
145///         println!("{:?}", Principal::to_text(i));
146///     }
147/// }
148/// ```
149pub async fn get_isp_admins(pem_identity_path: &str) -> Vec<candid::Principal> {
150    let canister_id = candid::Principal::from_text(ISP_CANISTER_ID_TEXT).unwrap();
151    let response_blob = build_agent(pem_identity_path)
152        .query(&canister_id, "getAdmins")
153        .with_arg(Encode!().expect("encode error"))
154        .call()
155        .await
156        .expect("response error");
157    let response = Decode!(&response_blob, Vec<candid::Principal>).unwrap();
158    response
159}
160
161/// Get ISP's ICSP_WASM version
162///
163/// # Examples
164///
165/// ``` no_run
166/// use isp_sdk::isp;
167///
168/// pub async fn get_isp_version() {
169///     println!(
170///         "isp version: {:?}",
171///         isp::get_version("identities/identity.pem").await
172///     );
173/// }
174/// ```
175pub async fn get_version(pem_identity_path: &str) -> String {
176    let canister_id = candid::Principal::from_text(ISP_CANISTER_ID_TEXT).unwrap();
177    let response_blob = build_agent(pem_identity_path)
178        .query(&canister_id, "getVersion")
179        .with_arg(Encode!().expect("encode error"))
180        .call()
181        .await
182        .expect("response error");
183    Decode!(&response_blob, String).unwrap()
184}
185
186/// Use icp to create a icsp canister and use the [XTC](https://github.com/Psychedelic/dank/tree/main/xtc) to top_up it
187///
188/// You must ensure that your subAccount has sufficient icp
189///
190/// And your pem Account have sufficient [XTC](https://github.com/Psychedelic/dank/tree/main/xtc)
191///
192/// Notice:
193///
194/// The icp_amount is e8s.
195/// 1 icp should be 100_000_000
196///
197/// The XTC is e12s.
198/// 1 T XTC(Cycles) should be 1_000_000_000_000
199///
200/// # Examples
201///
202/// ``` no_run
203/// use isp_sdk::isp::{self, CreateICSPResult};
204///
205/// pub async fn create_icsp() {
206///     let response = isp::create_icsp(
207///         "identities/identity.pem",
208///         "icsp-1",
209///         15_000_000 as u64,
210///         5_000_000_000_000 as u64 - 2_000_000_000 as u64,
211///     )
212///         .await;
213///     match response.0 {
214///         CreateICSPResult::ok(canister_id) => {
215///             println!("create icsp success: {:?}", canister_id.to_text());
216///             println!("use XTC topup result: {:?}", response.1.unwrap());
217///         }
218///         CreateICSPResult::err(error) => {
219///             println!("create icsp error: {:?}", error);
220///         }
221///     }
222/// }
223/// ```
224pub async fn create_icsp(
225    pem_identity_path: &str,
226    icsp_name: &str,
227    icp_to_create_amount: u64,
228    xtc_to_topup_amount: u64,
229) -> (CreateICSPResult, Option<BurnResult>) {
230    // create a icsp canister
231    let isp_canister_id = candid::Principal::from_text(ISP_CANISTER_ID_TEXT).unwrap();
232    let agent = build_agent(pem_identity_path);
233    let response_blob = agent
234        .update(&isp_canister_id, "createICSP")
235        .with_arg(Encode!(&icsp_name, &icp_to_create_amount).expect("encode error"))
236        .call_and_wait()
237        .await
238        .expect("response error");
239    let response = Decode!(&response_blob, CreateICSPResult).unwrap();
240    match response {
241        CreateICSPResult::ok(icsp_canister_id) => {
242            // use XTC to topup icsp
243            let top_up_response = top_up_icsp_with_xtc(
244                pem_identity_path,
245                BurnArgs {
246                    canister_id: icsp_canister_id,
247                    amount: xtc_to_topup_amount,
248                },
249            )
250            .await;
251            match top_up_response {
252                BurnResult::Ok(block_index) => {
253                    // init icsp
254                    let _init_response = agent
255                        .update(
256                            &candid::Principal::from_text(icsp_canister_id.to_text()).unwrap(),
257                            "init",
258                        )
259                        .with_arg(Encode!().expect("encode error"))
260                        .call_and_wait()
261                        .await
262                        .expect("response error");
263                    return (
264                        CreateICSPResult::ok(icsp_canister_id),
265                        Some(BurnResult::Ok(block_index)),
266                    );
267                }
268                BurnResult::Err(burn_err) => {
269                    return (
270                        CreateICSPResult::ok(icsp_canister_id),
271                        Some(BurnResult::Err(burn_err)),
272                    );
273                }
274            }
275        }
276        CreateICSPResult::err(create_err) => {
277            return (CreateICSPResult::err(create_err), None);
278        }
279    }
280}
281
282/// Transform icp to cycles and top_up tp icsp
283///
284/// # Examples
285///
286/// ``` no_run
287/// use candid::Principal;
288/// use isp_sdk::isp::{self, TopUpArgs};
289///
290/// pub async fn top_up_icsp() {
291///   println!(
292///      "topup icsp result:{:?}\n",
293///      isp::top_up_icsp(
294///          "identities/identity.pem",
295///          TopUpArgs {
296///             icsp_canisterId: Principal::from_text("xk2my-yqaaa-aaaal-abdwa-cai").unwrap(),
297///              icp_amount: 5_000_000 as u64,
298///          }
299///      )
300///           .await
301///   );
302/// }
303/// ```
304pub async fn top_up_icsp(pem_identity_path: &str, args: TopUpArgs) -> TopUpResult {
305    let canister_id = candid::Principal::from_text(ISP_CANISTER_ID_TEXT).unwrap();
306    let response_blob = build_agent(pem_identity_path)
307        .update(&canister_id, "topUpICSP")
308        .with_arg(Encode!(&args).expect("encode error"))
309        .call_and_wait()
310        .await
311        .expect("response error");
312    let response = Decode!(&response_blob, TopUpResult).unwrap();
313    response
314}
315
316#[derive(CandidType, Deserialize, Debug)]
317pub struct BurnArgs {
318    pub canister_id: candid::Principal,
319    pub amount: u64,
320}
321
322#[derive(CandidType, Deserialize, Debug)]
323pub enum BurnResult {
324    Ok(u64),
325    Err(BurnError),
326}
327
328#[derive(CandidType, Deserialize, Debug)]
329pub enum BurnError {
330    InsufficientBalance,
331    InvalidTokenContract,
332    NotSufficientLiquidity,
333}
334
335/// Use [XTC](https://github.com/Psychedelic/dank/tree/main/xtc) to top_up icsp
336///
337/// You must ensure you pem Account have sufficient [XTC](https://github.com/Psychedelic/dank/tree/main/xtc)
338///
339/// # Examples
340///
341/// ``` no_run
342/// use isp_sdk::isp::{self, BurnArgs};
343/// use candid::Principal;
344///
345/// pub async fn top_up_icsp_with_xtc() {
346///   println!(
347///       "topup icsp with XTC result:{:?}\n",
348///      isp::top_up_icsp_with_xtc(
349///         "identities/identity.pem",
350///         BurnArgs {
351///             canister_id: Principal::from_text("hf34l-eyaaa-aaaan-qav5q-cai").unwrap(),
352///            amount: 1_000_000_000_000 as u64 - 2_000_000_000 as u64,
353///         }
354///     )
355///          .await
356///  );
357/// }
358/// ```
359pub async fn top_up_icsp_with_xtc(pem_identity_path: &str, args: BurnArgs) -> BurnResult {
360    let canister_id = candid::Principal::from_text("aanaa-xaaaa-aaaah-aaeiq-cai").unwrap();
361    let response_blob = build_agent(pem_identity_path)
362        .update(&canister_id, "burn")
363        .with_arg(Encode!(&args).expect("encode error"))
364        .call_and_wait()
365        .await
366        .expect("response error");
367    let response = Decode!(&response_blob, BurnResult).unwrap();
368    response
369}
370
371fn get_waiter() -> Delay {
372    let waiter = garcon::Delay::builder()
373        .throttle(std::time::Duration::from_millis(500))
374        .timeout(std::time::Duration::from_secs(60 * 5))
375        .build();
376    waiter
377}
378
379fn build_agent(pem_identity_path: &str) -> Agent {
380    let url = "https://ic0.app".to_string();
381    let identity = Secp256k1Identity::from_pem_file(String::from(pem_identity_path)).unwrap();
382    let transport = ReqwestHttpReplicaV2Transport::create(url).expect("transport error");
383    let agent = Agent::builder()
384        .with_transport(transport)
385        .with_identity(identity)
386        .build()
387        .expect("build agent error");
388    agent
389}