ink_sandbox/api/
revive_api.rs

1use crate::{
2    AccountIdFor,
3    ContractExecResultFor,
4    ContractResultInstantiate,
5    H256,
6    Sandbox,
7    balance_to_evm_value,
8};
9use frame_support::{
10    pallet_prelude::DispatchError,
11    sp_runtime::traits::Bounded,
12    traits::{
13        Time,
14        fungible::{
15            Inspect,
16            Mutate,
17        },
18    },
19    weights::Weight,
20};
21use frame_system::pallet_prelude::OriginFor;
22use ink_primitives::Address;
23use pallet_revive::{
24    Code,
25    CodeUploadResult,
26    ExecConfig,
27    evm::{
28        Tracer,
29        TracerType,
30    },
31};
32use sp_core::U256;
33use std::ops::Not;
34
35type BalanceOf<R> =
36    <<R as pallet_revive::Config>::Currency as Inspect<AccountIdFor<R>>>::Balance;
37
38type MomentOf<T> = <<T as pallet_revive::Config>::Time as Time>::Moment;
39
40/// Contract API used to interact with `pallet-revive`.
41pub trait ContractAPI {
42    /// The runtime contract config.
43    type T: pallet_revive::Config;
44
45    /// Interface for `bare_instantiate` contract call with a simultaneous upload.
46    ///
47    /// # Arguments
48    ///
49    /// * `contract_bytes` - The contract code.
50    /// * `value` - The number of tokens to be transferred to the contract.
51    /// * `data` - The input data to be passed to the contract (including constructor
52    ///   name).
53    /// * `salt` - The salt to be used for contract address derivation.
54    /// * `origin` - The sender of the contract call.
55    /// * `gas_limit` - The gas limit for the contract call.
56    /// * `storage_deposit_limit` - The storage deposit limit for the contract call.
57    #[allow(clippy::type_complexity, clippy::too_many_arguments)]
58    fn map_account(&mut self, account: OriginFor<Self::T>) -> Result<(), DispatchError>;
59
60    /// `pallet-revive` uses a dedicated "pallet" account for tracking
61    /// storage deposits. The static account is returned by the
62    /// `pallet_revive::Pallet::account_id()` function.
63    ///
64    /// This function funds the account with the existential deposit
65    /// (i.e. minimum balance).
66    fn warm_up(&mut self);
67
68    /// Interface for `bare_instantiate` contract call with a simultaneous upload.
69    ///
70    /// # Arguments
71    ///
72    /// * `contract_bytes` - The contract code.
73    /// * `value` - The number of tokens to be transferred to the contract.
74    /// * `data` - The input data to be passed to the contract (including constructor
75    ///   name).
76    /// * `salt` - The salt to be used for contract address derivation.
77    /// * `origin` - The sender of the contract call.
78    /// * `gas_limit` - The gas limit for the contract call.
79    /// * `storage_deposit_limit` - The storage deposit limit for the contract call.
80    #[allow(clippy::type_complexity, clippy::too_many_arguments)]
81    fn deploy_contract(
82        &mut self,
83        contract_bytes: Vec<u8>,
84        value: BalanceOf<Self::T>,
85        data: Vec<u8>,
86        salt: Option<[u8; 32]>,
87        origin: OriginFor<Self::T>,
88        gas_limit: Weight,
89        storage_deposit_limit: BalanceOf<Self::T>,
90    ) -> ContractResultInstantiate<Self::T>;
91
92    /// Interface for `bare_instantiate` contract call for a previously uploaded contract.
93    ///
94    /// # Arguments
95    ///
96    /// * `code_hash` - The code hash of the contract to instantiate.
97    /// * `value` - The number of tokens to be transferred to the contract.
98    /// * `data` - The input data to be passed to the contract (including constructor
99    ///   name).
100    /// * `salt` - The salt to be used for contract address derivation.
101    /// * `origin` - The sender of the contract call.
102    /// * `gas_limit` - The gas limit for the contract call.
103    /// * `storage_deposit_limit` - The storage deposit limit for the contract call.
104    #[allow(clippy::type_complexity, clippy::too_many_arguments)]
105    fn instantiate_contract(
106        &mut self,
107        code_hash: H256,
108        value: BalanceOf<Self::T>,
109        data: Vec<u8>,
110        salt: Option<[u8; 32]>,
111        origin: OriginFor<Self::T>,
112        gas_limit: Weight,
113        storage_deposit_limit: BalanceOf<Self::T>,
114    ) -> ContractResultInstantiate<Self::T>;
115
116    /// Interface for `bare_upload_code` contract call.
117    ///
118    /// # Arguments
119    ///
120    /// * `contract_bytes` - The contract code.
121    /// * `origin` - The sender of the contract call.
122    /// * `storage_deposit_limit` - The storage deposit limit for the contract call.
123    fn upload_contract(
124        &mut self,
125        contract_bytes: Vec<u8>,
126        origin: OriginFor<Self::T>,
127        storage_deposit_limit: BalanceOf<Self::T>,
128    ) -> CodeUploadResult<BalanceOf<Self::T>>;
129
130    /// Interface for `bare_call` contract call.
131    ///
132    /// # Arguments
133    ///
134    /// * `address` - The address of the contract to be called.
135    /// * `value` - The number of tokens to be transferred to the contract.
136    /// * `data` - The input data to be passed to the contract (including message name).
137    /// * `origin` - The sender of the contract call.
138    /// * `gas_limit` - The gas limit for the contract call.
139    /// * `storage_deposit_limit` - The storage deposit limit for the contract call.
140    #[allow(clippy::type_complexity, clippy::too_many_arguments)]
141    fn call_contract(
142        &mut self,
143        address: Address,
144        value: BalanceOf<Self::T>,
145        data: Vec<u8>,
146        origin: OriginFor<Self::T>,
147        gas_limit: Weight,
148        storage_deposit_limit: BalanceOf<Self::T>,
149    ) -> ContractExecResultFor<Self::T>;
150
151    fn evm_tracer(&mut self, tracer_type: TracerType) -> Tracer<Self::T>;
152}
153
154impl<T> ContractAPI for T
155where
156    T: Sandbox,
157    T::Runtime: pallet_balances::Config + pallet_revive::Config,
158    BalanceOf<T::Runtime>: Into<U256> + TryFrom<U256> + Bounded,
159    MomentOf<T::Runtime>: Into<U256>,
160    <<T as Sandbox>::Runtime as frame_system::Config>::Nonce: Into<u32>,
161    // todo
162    <<T as Sandbox>::Runtime as frame_system::Config>::Hash:
163        frame_support::traits::IsType<sp_core::H256>,
164{
165    type T = T::Runtime;
166
167    fn map_account(
168        &mut self,
169        account_id: OriginFor<Self::T>,
170    ) -> Result<(), DispatchError> {
171        self.execute_with(|| pallet_revive::Pallet::<Self::T>::map_account(account_id))
172    }
173
174    /// `pallet-revive` uses a dedicated "pallet" account for tracking
175    /// storage deposits. The static account is returned by the
176    /// `pallet_revive::Pallet::account_id()` function.
177    ///
178    /// This function funds the account with the existential deposit
179    /// (i.e. minimum balance).
180    fn warm_up(&mut self) {
181        self.execute_with(|| {
182            let acc = pallet_revive::Pallet::<Self::T>::account_id();
183            let ed = pallet_balances::Pallet::<Self::T>::minimum_balance();
184
185            // we only fund the account if need be
186            if pallet_balances::Pallet::<Self::T>::free_balance(&acc) < ed {
187                let _ = pallet_balances::Pallet::<Self::T>::mint_into(&acc, ed);
188            }
189        });
190    }
191
192    fn deploy_contract(
193        &mut self,
194        contract_bytes: Vec<u8>,
195        value: BalanceOf<Self::T>,
196        data: Vec<u8>,
197        salt: Option<[u8; 32]>,
198        origin: OriginFor<Self::T>,
199        gas_limit: Weight,
200        storage_deposit_limit: BalanceOf<Self::T>,
201    ) -> ContractResultInstantiate<Self::T> {
202        self.warm_up();
203        self.execute_with(|| {
204            pallet_revive::Pallet::<Self::T>::bare_instantiate(
205                origin,
206                balance_to_evm_value::<Self::T>(value),
207                gas_limit,
208                storage_deposit_limit,
209                Code::Upload(contract_bytes),
210                data,
211                salt,
212                ExecConfig {
213                    bump_nonce: true,
214                    collect_deposit_from_hold: None,
215                    effective_gas_price: None,
216                    is_dry_run: false,
217                    mock_handler: None,
218                },
219            )
220        })
221    }
222
223    fn instantiate_contract(
224        &mut self,
225        code_hash: H256,
226        value: BalanceOf<Self::T>,
227        data: Vec<u8>,
228        salt: Option<[u8; 32]>,
229        origin: OriginFor<Self::T>,
230        gas_limit: Weight,
231        storage_deposit_limit: BalanceOf<Self::T>,
232    ) -> ContractResultInstantiate<Self::T> {
233        self.execute_with(|| {
234            pallet_revive::Pallet::<Self::T>::bare_instantiate(
235                origin,
236                balance_to_evm_value::<Self::T>(value),
237                gas_limit,
238                storage_deposit_limit,
239                Code::Existing(code_hash),
240                data,
241                salt,
242                ExecConfig {
243                    bump_nonce: true,
244                    collect_deposit_from_hold: None,
245                    effective_gas_price: None,
246                    is_dry_run: false,
247                    mock_handler: None,
248                },
249            )
250        })
251    }
252
253    fn upload_contract(
254        &mut self,
255        contract_bytes: Vec<u8>,
256        origin: OriginFor<Self::T>,
257        storage_deposit_limit: BalanceOf<Self::T>,
258    ) -> CodeUploadResult<BalanceOf<Self::T>> {
259        self.execute_with(|| {
260            pallet_revive::Pallet::<Self::T>::bare_upload_code(
261                origin,
262                contract_bytes,
263                storage_deposit_limit,
264            )
265        })
266    }
267
268    fn call_contract(
269        &mut self,
270        address: Address,
271        value: BalanceOf<Self::T>,
272        data: Vec<u8>,
273        origin: OriginFor<Self::T>,
274        gas_limit: Weight,
275        storage_deposit_limit: BalanceOf<Self::T>,
276    ) -> ContractExecResultFor<Self::T> {
277        self.execute_with(|| {
278            pallet_revive::Pallet::<Self::T>::bare_call(
279                origin,
280                address,
281                balance_to_evm_value::<Self::T>(value),
282                gas_limit,
283                storage_deposit_limit,
284                data,
285                ExecConfig {
286                    bump_nonce: true,
287                    collect_deposit_from_hold: None,
288                    effective_gas_price: None,
289                    is_dry_run: false,
290                    mock_handler: None,
291                },
292            )
293        })
294    }
295
296    fn evm_tracer(&mut self, tracer_type: TracerType) -> Tracer<Self::T> {
297        self.execute_with(|| pallet_revive::Pallet::<Self::T>::evm_tracer(tracer_type))
298    }
299}
300
301/// todo
302/// Converts bytes to a '\n'-split string, ignoring empty lines.
303pub fn decode_debug_buffer(buffer: &[u8]) -> Vec<String> {
304    let decoded = buffer.iter().map(|b| *b as char).collect::<String>();
305    decoded
306        .split('\n')
307        .filter_map(|s| s.is_empty().not().then_some(s.to_string()))
308        .collect()
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::{
315        DefaultSandbox,
316        RuntimeEventOf,
317        api::prelude::*,
318    };
319
320    const STORAGE_DEPOSIT_LIMIT: u128 = u128::MAX;
321
322    fn compile_module(contract_name: &str) -> Vec<u8> {
323        // todo compile the contract, instead of reading the binary
324        let path = [
325            std::env::var("CARGO_MANIFEST_DIR").as_deref().unwrap(),
326            "/test-resources/",
327            contract_name,
328            ".polkavm",
329        ]
330        .concat();
331        std::fs::read(std::path::Path::new(&path)).unwrap()
332    }
333
334    /// `pallet-revive` uses a dedicated "pallet" account for tracking
335    /// storage deposits. The static account is returned by the
336    /// `pallet_revive::Pallet::account_id()` function.
337    ///
338    /// This function funds the account with the existential deposit
339    /// (i.e. minimum balance).
340    fn warm_up<T>(sandbox: &mut T)
341    where
342        <T as Sandbox>::Runtime: pallet_revive::Config + pallet_balances::Config,
343        T: BalanceAPI<T> + Sandbox,
344    {
345        let acc = pallet_revive::Pallet::<<T as Sandbox>::Runtime>::account_id();
346        let ed = pallet_balances::Pallet::<<T as Sandbox>::Runtime>::minimum_balance();
347        sandbox.mint_into(&acc, ed).unwrap_or_else(|_| {
348            panic!("Failed to mint existential balance into `pallet-revive` account")
349        });
350    }
351
352    #[test]
353    fn can_upload_code() {
354        let mut sandbox = DefaultSandbox::default();
355        let contract_binary = compile_module("dummy");
356        warm_up::<DefaultSandbox>(&mut sandbox);
357
358        use sha3::{
359            Digest,
360            Keccak256,
361        };
362        let hash = Keccak256::digest(contract_binary.as_slice());
363        let hash = H256::from_slice(hash.as_slice());
364
365        let origin =
366            DefaultSandbox::convert_account_to_origin(DefaultSandbox::default_actor());
367        let result = sandbox.upload_contract(contract_binary, origin, 100000000000000);
368
369        assert!(result.is_ok());
370        assert_eq!(hash, result.unwrap().code_hash);
371    }
372
373    #[test]
374    fn can_deploy_contract() {
375        let mut sandbox = DefaultSandbox::default();
376        let contract_binary = compile_module("dummy");
377
378        let events_before = sandbox.events();
379        assert!(events_before.is_empty());
380
381        warm_up::<DefaultSandbox>(&mut sandbox);
382
383        let origin =
384            DefaultSandbox::convert_account_to_origin(DefaultSandbox::default_actor());
385        sandbox.map_account(origin.clone()).expect("cannot map");
386        let result = sandbox.deploy_contract(
387            contract_binary.clone(),
388            0,
389            vec![],
390            None,
391            origin.clone(),
392            DefaultSandbox::default_gas_limit(),
393            100000000000000,
394        );
395        assert!(result.result.is_ok());
396        assert!(!result.result.unwrap().result.did_revert());
397
398        // deploying again must fail due to `DuplicateContract`
399        let result = sandbox.deploy_contract(
400            contract_binary,
401            0,
402            vec![],
403            None,
404            origin,
405            DefaultSandbox::default_gas_limit(),
406            100000000000000,
407        );
408        assert!(result.result.is_err());
409        let dispatch_err = result.result.unwrap_err();
410        assert!(format!("{dispatch_err:?}").contains("DuplicateContract"));
411    }
412
413    #[test]
414    fn can_call_contract() {
415        let mut sandbox = DefaultSandbox::default();
416        let _actor = DefaultSandbox::default_actor();
417        let contract_binary = compile_module("dummy");
418        warm_up::<DefaultSandbox>(&mut sandbox);
419
420        let origin =
421            DefaultSandbox::convert_account_to_origin(DefaultSandbox::default_actor());
422        sandbox.map_account(origin.clone()).expect("unable to map");
423        let result = sandbox.deploy_contract(
424            contract_binary,
425            0,
426            vec![],
427            None,
428            origin.clone(),
429            DefaultSandbox::default_gas_limit(),
430            STORAGE_DEPOSIT_LIMIT,
431        );
432        assert!(!result.result.clone().unwrap().result.did_revert());
433
434        let contract_address = result.result.expect("Contract should be deployed").addr;
435
436        sandbox.reset_events();
437
438        let result = sandbox.call_contract(
439            contract_address,
440            0,
441            vec![],
442            origin.clone(),
443            DefaultSandbox::default_gas_limit(),
444            STORAGE_DEPOSIT_LIMIT,
445        );
446        assert!(result.result.is_ok());
447        assert!(!result.result.unwrap().did_revert());
448
449        let events = sandbox.events();
450        assert_eq!(events.len(), 1);
451        assert_eq!(
452            events[0].event,
453            RuntimeEventOf::<DefaultSandbox>::Revive(
454                pallet_revive::Event::ContractEmitted {
455                    contract: contract_address,
456                    topics: vec![H256::from([42u8; 32])],
457                    data: vec![1, 2, 3, 4],
458                }
459            )
460        );
461    }
462}