pink_subrpc/contracts/
mod.rs

1use alloc::vec::Vec;
2use objects::*;
3use pink::chain_extension::signing;
4use primitive_types::H256;
5use scale::{Decode, Encode};
6
7use crate::contracts::objects::{ContractCall, WeightV2};
8
9pub mod objects;
10
11pub type ContractId = [u8; 32];
12pub type Balance = u128;
13
14#[derive(Encode, Decode, Debug)]
15#[repr(u8)]
16#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
17pub enum Error {
18    FailedToDryRunContract(crate::Error),
19    FailedToQueryContract(crate::Error),
20    FailedToCreateTransaction(crate::Error),
21    FailedToSendTransaction(crate::Error),
22    FailedToDecode,
23    InvalidAddressLength,
24    NoResult,
25    FailedToReadResult,
26    ContractError(Vec<u8>), // the contract explicitly returns an error (error flag == 256)
27    ContractUnknownError,   // the contract explicitly returns an error (error flag == 256) but cannot decode the error
28    ContractTrapped(u32),   // contract panicked: error flag <> 0 && error flag <> 256
29}
30
31#[derive(Encode, Decode, Debug)]
32#[repr(u8)]
33#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
34pub enum ContractError {
35    Error,
36}
37
38pub type Result<T> = core::result::Result<T, Error>;
39
40pub struct InkContract<'a> {
41    rpc: &'a str,
42    pallet_id: u8,
43    call_id: u8,
44    contract_id: &'a ContractId,
45}
46
47impl<'a> InkContract<'a> {
48    pub fn new(rpc: &'a str, pallet_id: u8, call_id: u8, contract_id: &'a ContractId) -> Self {
49        InkContract {
50            rpc,
51            pallet_id,
52            call_id,
53            contract_id,
54        }
55    }
56
57    pub fn query<A: Encode, R: Decode>(
58        &self,
59        origin: [u8; 32],
60        contract_method: [u8; 4],
61        contract_args: Option<&A>,
62        value: Balance,
63    ) -> Result<R> {
64        self.query_at(origin, contract_method, contract_args, value, None)
65    }
66
67    pub fn query_at<A: Encode, R: Decode>(
68        &self,
69        origin: [u8; 32],
70        contract_method: [u8; 4],
71        contract_args: Option<&A>,
72        value: Balance,
73        at: Option<H256>,
74    ) -> Result<R> {
75        let call = build_contract_query(
76            origin,
77            *self.contract_id,
78            contract_method,
79            contract_args,
80            value,
81        );
82
83        let encoded_call = Encode::encode(&call);
84
85        let contract_query_result: ContractQueryResult<ContractError, Balance> =
86            crate::query_contract(self.rpc, &encoded_call, at)
87                .map_err(Error::FailedToQueryContract)?;
88
89        // manage the errors
90        self.handle_error(&contract_query_result)?;
91
92        // no error => the contract succeeds
93        let result = contract_query_result
94            .result
95            .data
96            .map_err(|_| Error::NoResult)?;
97        let result = <core::result::Result<R, Error>>::decode(&mut result.as_slice())
98            .map_err(|_| Error::FailedToDecode)?;
99        let result = result.map_err(|_| Error::FailedToReadResult)?;
100
101        Ok(result)
102    }
103
104    pub fn send_transaction<A: Encode>(
105        &self,
106        contract_method: [u8; 4],
107        contract_args: Option<&A>,
108        value: Balance,
109        gas_limit: WeightV2,
110        signer: &[u8; 32],
111    ) -> Result<Vec<u8>> {
112        let call: ContractCall<ContractId, u32, Balance> = build_contract_call(
113            *self.contract_id,
114            contract_method,
115            contract_args,
116            value,
117            gas_limit,
118        );
119
120        let signed_tx = crate::create_transaction(
121            signer,
122            "astar",
123            self.rpc,
124            self.pallet_id,
125            self.call_id,
126            call,
127            crate::ExtraParam::default(),
128        )
129        .map_err(Error::FailedToCreateTransaction)?;
130
131        let result = crate::send_transaction(self.rpc, &signed_tx)
132            .map_err(Error::FailedToSendTransaction)?;
133
134        Ok(result)
135    }
136
137    pub fn dry_run_and_send_transaction<A: Encode>(
138        &self,
139        contract_method: [u8; 4],
140        contract_args: Option<&A>,
141        value: Balance,
142        signer: &[u8; 32],
143    ) -> Result<Vec<u8>> {
144        let origin: [u8; 32] = signing::get_public_key(signer, signing::SigType::Sr25519)
145            .try_into()
146            .map_err(|_| Error::InvalidAddressLength)?;
147
148        let call = build_contract_query(
149            origin,
150            *self.contract_id,
151            contract_method,
152            contract_args,
153            value,
154        );
155
156        let encoded_call = Encode::encode(&call);
157
158        let contract_query_result: ContractQueryResult<ContractError, Balance> =
159            crate::query_contract(self.rpc, &encoded_call, None)
160                .map_err(Error::FailedToQueryContract)?;
161
162        // manage the errors
163        self.handle_error(&contract_query_result)?;
164
165        // no error when query => we can send teh transaction
166        self.send_transaction(
167            contract_method,
168            contract_args,
169            value,
170            contract_query_result.gas_required,
171            signer,
172        )
173    }
174
175    fn handle_error(
176        &self,
177        contract_query_result: &ContractQueryResult<ContractError, Balance>,
178    ) -> Result<()> {
179        // flags == 0 => the contract succeeds
180        if contract_query_result.result.flags == 0 {
181            return Ok(());
182        }
183
184        // flags == 256 => the contract returns a managed error (ie error expected by the contract)
185        if contract_query_result.result.flags == 256 {
186            let error = match &contract_query_result.result.data {
187                Ok(v) => Error::ContractError(v.clone()),
188                _ => Error::ContractUnknownError,
189            };
190            return Err(error);
191        }
192        // otherwise, the contracts panicked
193        Err(Error::ContractTrapped(contract_query_result.result.flags))
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    /// Here the contract deployed on Shibuya (Astar testnet)
202    /// contract Id : d0859843adc542e9439152c9a17e8cf5260c644346334b1a38dd624a7fb24af6
203    ///
204    /// #[ink::contract]
205    /// mod incrementer {
206    ///
207    ///     #[ink(storage)]
208    ///     pub struct Incrementer {
209    ///         value: i32,
210    ///     }
211    ///
212    ///     #[ink(event)]
213    ///     pub struct Incremented {
214    ///         by: i32,
215    ///         new_value: i32,
216    ///         who: AccountId,
217    ///     }
218    ///
219    ///     /// Errors occurred in the contract
220    ///     #[derive(scale::Encode, scale::Decode)]
221    ///     #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
222    ///     pub enum Error {
223    ///         WillfulError
224    ///     }
225    ///
226    ///     impl Incrementer {
227    ///
228    ///         #[ink(constructor)]
229    ///         pub fn new(init_value: i32) -> Self {
230    ///             Self { value: init_value }
231    ///         }
232    ///
233    ///         #[ink(message)]
234    ///         pub fn inc_by_1(&mut self) -> Result<(), Error> {
235    ///             self.inc(1);
236    ///             Ok(())
237    ///         }
238    ///
239    ///         #[ink(message)]
240    ///         pub fn inc(&mut self, by: i32) {
241    ///             self.value += by;
242    ///             ink::env::debug_println!("increment by {}, new value {}", by, self.value);
243    ///             let signer = self.env().caller();
244    ///             self.env().emit_event(Incremented{ by, new_value: self.value, who: signer });
245    ///         }
246    ///
247    ///         #[ink(message)]
248    ///         pub fn get(&self) -> i32 {
249    ///             self.value
250    ///         }
251    ///
252    ///         #[ink(message)]
253    ///         pub fn get_with_result(&self) -> Result<i32, Error> {
254    ///             Ok(self.value)
255    ///         }
256    ///
257    ///         #[ink(message)]
258    ///         pub fn get_error(&self) -> Result<(), Error> {
259    ///             Err(Error::WillfulError)
260    ///         }
261    ///
262    ///         #[ink(message)]
263    ///         pub fn panic(&self) -> Result<(), Error> {
264    ///             panic!("For test");
265    ///         }
266    ///
267    ///     }
268    /// }
269    ///
270    struct EnvVars {
271        /// The RPC endpoint of the target blockchain
272        rpc: String,
273        pallet_id: u8,
274        call_id: u8,
275        contract_id: ContractId,
276    }
277
278    fn env() -> EnvVars {
279        // local node
280        /*
281        EnvVars {
282            rpc: "http://127.0.0.1:9944".to_string(),
283            pallet_id: 7u8,
284            call_id: 6u8,
285            contract_id: hex_literal::hex!("14dca26ea5e235f71373a6b44752ee3e63b3bed2b68e8e5cce0ec9b486d59dab"),
286        }
287         */
288        // shibuya
289        EnvVars {
290            rpc: "https://shibuya.public.blastapi.io".to_string(),
291            pallet_id: 70u8,
292            call_id: 6u8,
293            contract_id: hex_literal::hex!(
294                "d0859843adc542e9439152c9a17e8cf5260c644346334b1a38dd624a7fb24af6"
295            ),
296        }
297    }
298
299    #[test]
300    #[ignore = "this is expensive so we don't test it often"]
301    fn test_query_with_primitive_result() {
302        pink_chain_extension::mock_ext::mock_all_ext();
303
304        // get the environment variables
305        let EnvVars {
306            rpc,
307            pallet_id,
308            call_id,
309            contract_id,
310        } = env();
311
312        // create the struct to interact with the smart contract
313        let contract = InkContract::new(&rpc, pallet_id, call_id, &contract_id);
314
315        // address who performs the query
316        let origin =
317            hex_literal::hex!("189dac29296d31814dc8c56cf3d36a0543372bba7538fa322a4aebfebc39e056");
318        // method to call:
319        // "label": "get",
320        // "selector": "0x2f865bd9"
321        let method_get = hex_literal::hex!("2f865bd9");
322        // no argument
323        let params: Option<&()> = None;
324
325        // call the method
326        let value: i32 = contract
327            .query(origin, method_get, params, 0)
328            .expect("Error when call the method 'get'");
329        // display the result
330        println!("Query the method get, result : {}", value);
331        assert!(value > 0);
332    }
333
334    #[test]
335    #[ignore = "this is expensive so we don't test it often"]
336    fn test_query_with_object_result() {
337        pink_chain_extension::mock_ext::mock_all_ext();
338
339        // get the environment variables
340        let EnvVars {
341            rpc,
342            pallet_id,
343            call_id,
344            contract_id,
345        } = env();
346
347        // create the struct to interact with the smart contract
348        let contract = InkContract::new(&rpc, pallet_id, call_id, &contract_id);
349
350        // address who performs the query
351        let origin =
352            hex_literal::hex!("189dac29296d31814dc8c56cf3d36a0543372bba7538fa322a4aebfebc39e056");
353        // method to call:
354        //  "label": "get_with_result",
355        //  "selector": "0xf21dd3cb"
356        let method_get_with_result = hex_literal::hex!("f21dd3cb");
357        // no argument
358        let params: Option<&()> = None;
359
360        // result of the query
361        type Result = core::result::Result<i32, Error>;
362        // call the method
363        let value: Result = contract
364            .query(origin, method_get_with_result, params, 0)
365            .expect("Error when call the method 'get_with_result'");
366
367        // display the result
368        println!("Query the method get, result : {:?}", value);
369        assert!(value.unwrap() > 0);
370    }
371
372    #[test]
373    #[ignore = "this is expensive so we don't test it often"]
374    fn test_call_without_params() {
375        pink_chain_extension::mock_ext::mock_all_ext();
376
377        // get the environment variables
378        let EnvVars {
379            rpc,
380            pallet_id,
381            call_id,
382            contract_id,
383        } = env();
384
385        // create the struct to interact with the smart contract
386        let contract = InkContract::new(&rpc, pallet_id, call_id, &contract_id);
387
388        // Secret key of test account `//Alice`
389        let alice_pk: [u8; 32] =
390            hex_literal::hex!("e5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a");
391
392        // method to call:
393        //         "label": "inc_by_1",
394        //         "selector": "0xb5d14a10"
395        let method_inc_by_1 = hex_literal::hex!("b5d14a10");
396        // no argument
397        let params: Option<&()> = None;
398
399        // call the method
400        contract
401            .dry_run_and_send_transaction(method_inc_by_1, params, 0, &alice_pk)
402            .expect("Error when call the method 'inc'");
403    }
404
405    #[test]
406    #[ignore = "this is expensive so we don't test it often"]
407    fn test_call_with_params() {
408        pink_chain_extension::mock_ext::mock_all_ext();
409
410        // get the environment variables
411        let EnvVars {
412            rpc,
413            pallet_id,
414            call_id,
415            contract_id,
416        } = env();
417
418        // create the struct to interact with the smart contract
419        let contract = InkContract::new(&rpc, pallet_id, call_id, &contract_id);
420
421        // Secret key of test account `//Alice`
422        let alice_pk: [u8; 32] =
423            hex_literal::hex!("e5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a");
424
425        // method to call:
426        //         "label": "inc",
427        //         "selector": "0x1d32619f"
428        let method_inc = hex_literal::hex!("1d32619f");
429        // argument
430        let params: Option<&i32> = Some(&3);
431
432        // call the method
433        contract
434            .dry_run_and_send_transaction(method_inc, params, 0, &alice_pk)
435            .expect("Error when call the method 'inc'");
436    }
437
438    #[test]
439    #[ignore = "this is expensive so we don't test it often"]
440    fn test_query_with_error() {
441        pink_chain_extension::mock_ext::mock_all_ext();
442
443        // get the environment variables
444        let EnvVars {
445            rpc,
446            pallet_id,
447            call_id,
448            contract_id,
449        } = env();
450
451        // create the struct to interact with the smart contract
452        let contract = InkContract::new(&rpc, pallet_id, call_id, &contract_id);
453
454        // address who performs the query
455        let origin =
456            hex_literal::hex!("189dac29296d31814dc8c56cf3d36a0543372bba7538fa322a4aebfebc39e056");
457        // method to call:
458        //         "label": "get_error",
459        //         "selector": "0x6baa1eed"
460        let method_get_error = hex_literal::hex!("6baa1eed");
461        // no argument
462        let params: Option<&()> = None;
463        // result of the query
464        type Result = core::result::Result<i32, Error>;
465        // call the method
466        let result: Result = contract.query(origin, method_get_error, params, 0);
467        match result {
468            Err(Error::ContractError(e)) => println!("Expected contract error {:?}", e),
469            Err(e) => {
470                println!(
471                    "We expect to receive a contract error but we receive this error {:?}",
472                    e
473                );
474                panic!("we expect to receive a contract error");
475            }
476            r => {
477                println!("We expect to receive an error but we receive that {:?}", r);
478                panic!("we expect to receive an error");
479            }
480        }
481    }
482
483    #[test]
484    #[ignore = "this is expensive so we don't test it often"]
485    fn test_query_with_trapped_error() {
486        pink_chain_extension::mock_ext::mock_all_ext();
487
488        // get the environment variables
489        let EnvVars {
490            rpc,
491            pallet_id,
492            call_id,
493            contract_id,
494        } = env();
495
496        // create the struct to interact with the smart contract
497        let contract = InkContract::new(&rpc, pallet_id, call_id, &contract_id);
498
499        // address who performs the query
500        let origin =
501            hex_literal::hex!("189dac29296d31814dc8c56cf3d36a0543372bba7538fa322a4aebfebc39e056");
502        // method to call:
503        //         "label": "panic",
504        //         "selector": "0xfb02c510"
505        let method_get_error = hex_literal::hex!("fb02c510");
506        // no argument
507        let params: Option<&()> = None;
508        // result of the query
509        type Result = core::result::Result<i32, Error>;
510        // call the method
511        let result: Result = contract.query(origin, method_get_error, params, 0);
512        match result {
513            Err(Error::ContractTrapped(e)) => println!("Expected contract trapped error {:?}", e),
514            Err(e) => {
515                println!(
516                    "We expect to receive a contract trapped error but we receive this error {:?}",
517                    e
518                );
519                panic!("we expect to receive a contract trapped error");
520            }
521            r => {
522                println!("We expect to receive an error but we receive that {:?}", r);
523                panic!("we expect to receive an error");
524            }
525        }
526    }
527
528    #[test]
529    #[ignore = "this is expensive so we don't test it often"]
530    fn test_call_with_error() {
531        pink_chain_extension::mock_ext::mock_all_ext();
532
533        // get the environment variables
534        let EnvVars {
535            rpc,
536            pallet_id,
537            call_id,
538            contract_id,
539        } = env();
540
541        // create the struct to interact with the smart contract
542        let contract = InkContract::new(&rpc, pallet_id, call_id, &contract_id);
543
544        // address who performs the query
545        let alice_pk: [u8; 32] =
546            hex_literal::hex!("e5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a");
547        // method to call:
548        //         "label": "get_error",
549        //         "selector": "0x721bc303"
550        let method_get_error = hex_literal::hex!("721bc303");
551        // no argument
552        let params: Option<&()> = None;
553
554        // call the method
555        let result = contract.dry_run_and_send_transaction(method_get_error, params, 0, &alice_pk);
556        match result {
557            Err(e) => println!("Expected error {:?}", e),
558            r => {
559                println!("We expect to receive an error but we receive that {:?}", r);
560                panic!("we expect to receive an error");
561            }
562        }
563    }
564}