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: false,
215                    effective_gas_price: None,
216                },
217            )
218        })
219    }
220
221    fn instantiate_contract(
222        &mut self,
223        code_hash: H256,
224        value: BalanceOf<Self::T>,
225        data: Vec<u8>,
226        salt: Option<[u8; 32]>,
227        origin: OriginFor<Self::T>,
228        gas_limit: Weight,
229        storage_deposit_limit: BalanceOf<Self::T>,
230    ) -> ContractResultInstantiate<Self::T> {
231        self.execute_with(|| {
232            pallet_revive::Pallet::<Self::T>::bare_instantiate(
233                origin,
234                balance_to_evm_value::<Self::T>(value),
235                gas_limit,
236                storage_deposit_limit,
237                Code::Existing(code_hash),
238                data,
239                salt,
240                ExecConfig {
241                    bump_nonce: true,
242                    collect_deposit_from_hold: false,
243                    effective_gas_price: None,
244                },
245            )
246        })
247    }
248
249    fn upload_contract(
250        &mut self,
251        contract_bytes: Vec<u8>,
252        origin: OriginFor<Self::T>,
253        storage_deposit_limit: BalanceOf<Self::T>,
254    ) -> CodeUploadResult<BalanceOf<Self::T>> {
255        self.execute_with(|| {
256            pallet_revive::Pallet::<Self::T>::bare_upload_code(
257                origin,
258                contract_bytes,
259                storage_deposit_limit,
260            )
261        })
262    }
263
264    fn call_contract(
265        &mut self,
266        address: Address,
267        value: BalanceOf<Self::T>,
268        data: Vec<u8>,
269        origin: OriginFor<Self::T>,
270        gas_limit: Weight,
271        storage_deposit_limit: BalanceOf<Self::T>,
272    ) -> ContractExecResultFor<Self::T> {
273        self.execute_with(|| {
274            pallet_revive::Pallet::<Self::T>::bare_call(
275                origin,
276                address,
277                balance_to_evm_value::<Self::T>(value),
278                gas_limit,
279                storage_deposit_limit,
280                data,
281                ExecConfig {
282                    bump_nonce: true,
283                    collect_deposit_from_hold: false,
284                    effective_gas_price: None,
285                },
286            )
287        })
288    }
289
290    fn evm_tracer(&mut self, tracer_type: TracerType) -> Tracer<Self::T> {
291        self.execute_with(|| pallet_revive::Pallet::<Self::T>::evm_tracer(tracer_type))
292    }
293}
294
295/// todo
296/// Converts bytes to a '\n'-split string, ignoring empty lines.
297pub fn decode_debug_buffer(buffer: &[u8]) -> Vec<String> {
298    let decoded = buffer.iter().map(|b| *b as char).collect::<String>();
299    decoded
300        .split('\n')
301        .filter_map(|s| s.is_empty().not().then_some(s.to_string()))
302        .collect()
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::{
309        DefaultSandbox,
310        RuntimeEventOf,
311        api::prelude::*,
312    };
313
314    const STORAGE_DEPOSIT_LIMIT: u128 = u128::MAX;
315
316    fn compile_module(contract_name: &str) -> Vec<u8> {
317        // todo compile the contract, instead of reading the binary
318        let path = [
319            std::env::var("CARGO_MANIFEST_DIR").as_deref().unwrap(),
320            "/test-resources/",
321            contract_name,
322            ".polkavm",
323        ]
324        .concat();
325        std::fs::read(std::path::Path::new(&path)).unwrap()
326    }
327
328    /// `pallet-revive` uses a dedicated "pallet" account for tracking
329    /// storage deposits. The static account is returned by the
330    /// `pallet_revive::Pallet::account_id()` function.
331    ///
332    /// This function funds the account with the existential deposit
333    /// (i.e. minimum balance).
334    fn warm_up<T>(sandbox: &mut T)
335    where
336        <T as Sandbox>::Runtime: pallet_revive::Config + pallet_balances::Config,
337        T: BalanceAPI<T> + Sandbox,
338    {
339        let acc = pallet_revive::Pallet::<<T as Sandbox>::Runtime>::account_id();
340        let ed = pallet_balances::Pallet::<<T as Sandbox>::Runtime>::minimum_balance();
341        sandbox.mint_into(&acc, ed).unwrap_or_else(|_| {
342            panic!("Failed to mint existential balance into `pallet-revive` account")
343        });
344    }
345
346    #[test]
347    fn can_upload_code() {
348        let mut sandbox = DefaultSandbox::default();
349        let contract_binary = compile_module("dummy");
350        warm_up::<DefaultSandbox>(&mut sandbox);
351
352        use sha3::{
353            Digest,
354            Keccak256,
355        };
356        let hash = Keccak256::digest(contract_binary.as_slice());
357        let hash = H256::from_slice(hash.as_slice());
358
359        let origin =
360            DefaultSandbox::convert_account_to_origin(DefaultSandbox::default_actor());
361        let result = sandbox.upload_contract(contract_binary, origin, 100000000000000);
362
363        assert!(result.is_ok());
364        assert_eq!(hash, result.unwrap().code_hash);
365    }
366
367    #[test]
368    fn can_deploy_contract() {
369        let mut sandbox = DefaultSandbox::default();
370        let contract_binary = compile_module("dummy");
371
372        let events_before = sandbox.events();
373        assert!(events_before.is_empty());
374
375        warm_up::<DefaultSandbox>(&mut sandbox);
376
377        let origin =
378            DefaultSandbox::convert_account_to_origin(DefaultSandbox::default_actor());
379        sandbox.map_account(origin.clone()).expect("cannot map");
380        let result = sandbox.deploy_contract(
381            contract_binary.clone(),
382            0,
383            vec![],
384            None,
385            origin.clone(),
386            DefaultSandbox::default_gas_limit(),
387            100000000000000,
388        );
389        assert!(result.result.is_ok());
390        assert!(!result.result.unwrap().result.did_revert());
391
392        // deploying again must fail due to `DuplicateContract`
393        let result = sandbox.deploy_contract(
394            contract_binary,
395            0,
396            vec![],
397            None,
398            origin,
399            DefaultSandbox::default_gas_limit(),
400            100000000000000,
401        );
402        assert!(result.result.is_err());
403        let dispatch_err = result.result.unwrap_err();
404        assert!(format!("{dispatch_err:?}").contains("DuplicateContract"));
405    }
406
407    #[test]
408    fn can_call_contract() {
409        let mut sandbox = DefaultSandbox::default();
410        let _actor = DefaultSandbox::default_actor();
411        let contract_binary = compile_module("dummy");
412        warm_up::<DefaultSandbox>(&mut sandbox);
413
414        let origin =
415            DefaultSandbox::convert_account_to_origin(DefaultSandbox::default_actor());
416        sandbox.map_account(origin.clone()).expect("unable to map");
417        let result = sandbox.deploy_contract(
418            contract_binary,
419            0,
420            vec![],
421            None,
422            origin.clone(),
423            DefaultSandbox::default_gas_limit(),
424            STORAGE_DEPOSIT_LIMIT,
425        );
426        assert!(!result.result.clone().unwrap().result.did_revert());
427
428        let contract_address = result.result.expect("Contract should be deployed").addr;
429
430        sandbox.reset_events();
431
432        let result = sandbox.call_contract(
433            contract_address,
434            0,
435            vec![],
436            origin.clone(),
437            DefaultSandbox::default_gas_limit(),
438            STORAGE_DEPOSIT_LIMIT,
439        );
440        assert!(result.result.is_ok());
441        assert!(!result.result.unwrap().did_revert());
442
443        let events = sandbox.events();
444        assert_eq!(events.len(), 1);
445        assert_eq!(
446            events[0].event,
447            RuntimeEventOf::<DefaultSandbox>::Revive(
448                pallet_revive::Event::ContractEmitted {
449                    contract: contract_address,
450                    topics: vec![H256::from([42u8; 32])],
451                    data: vec![1, 2, 3, 4],
452                }
453            )
454        );
455    }
456}