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