ink_e2e/
sandbox_client.rs

1// Copyright (C) Use Ink (UK) Ltd.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::{
16    backend::BuilderClient,
17    builders::{
18        constructor_exec_input,
19        CreateBuilderPartial,
20    },
21    client_utils::{
22        salt,
23        ContractsRegistry,
24    },
25    contract_results::{
26        BareInstantiationResult,
27        ContractExecResultFor,
28        ContractResult,
29    },
30    error::SandboxErr,
31    log_error,
32    CallBuilderFinal,
33    CallDryRunResult,
34    ChainBackend,
35    ContractsBackend,
36    E2EBackend,
37    InstantiateDryRunResult,
38    UploadResult,
39    H256,
40};
41use frame_support::{
42    dispatch::RawOrigin,
43    pallet_prelude::DispatchError,
44    traits::{
45        fungible::Inspect,
46        IsType,
47    },
48};
49use ink_env::Environment;
50use ink_primitives::{
51    reflect::{
52        AbiDecodeWith,
53        AbiEncodeWith,
54    },
55    DepositLimit,
56};
57use ink_sandbox::{
58    api::prelude::*,
59    frame_system,
60    frame_system::pallet_prelude::OriginFor,
61    pallet_balances,
62    pallet_revive,
63    AccountIdFor,
64    RuntimeCall,
65    Sandbox,
66    Weight,
67};
68use jsonrpsee::core::async_trait;
69use pallet_revive::{
70    evm::{
71        CallTrace,
72        TracerConfig,
73        U256,
74    },
75    CodeUploadReturnValue,
76    InstantiateReturnValue,
77    MomentOf,
78};
79use scale::Decode;
80use sp_core::{
81    sr25519::Pair,
82    Pair as _,
83};
84use sp_runtime::traits::Bounded;
85use std::{
86    marker::PhantomData,
87    path::PathBuf,
88};
89use subxt::{
90    dynamic::Value,
91    tx::Payload,
92};
93use subxt_signer::sr25519::Keypair;
94
95type BalanceOf<R> = <R as pallet_balances::Config>::Balance;
96type ContractsBalanceOf<R> =
97    <<R as pallet_revive::Config>::Currency as Inspect<AccountIdFor<R>>>::Balance;
98
99pub struct Client<AccountId, S: Sandbox> {
100    sandbox: S,
101    contracts: ContractsRegistry,
102    _phantom: PhantomData<AccountId>,
103}
104
105// While it is not necessary true that `Client` is `Send`, it will not be used in a way
106// that would violate this bound. In particular, all `Client` instances will be operating
107// synchronously.
108unsafe impl<AccountId, S: Sandbox> Send for Client<AccountId, S> {}
109impl<AccountId, S: Sandbox> Client<AccountId, S>
110where
111    S: Default,
112    S::Runtime: pallet_balances::Config + pallet_revive::Config,
113    AccountIdFor<S::Runtime>: From<[u8; 32]>,
114    BalanceOf<S::Runtime>: From<u128>,
115{
116    pub fn new<P: Into<PathBuf>>(contracts: impl IntoIterator<Item = P>) -> Self {
117        let mut sandbox = S::default();
118        Self::fund_accounts(&mut sandbox);
119
120        Self {
121            sandbox,
122            contracts: ContractsRegistry::new(contracts),
123            _phantom: Default::default(),
124        }
125    }
126
127    fn fund_accounts(sandbox: &mut S) {
128        const TOKENS: u128 = 1_000_000_000_000_000;
129
130        let accounts = [
131            crate::alice(),
132            crate::bob(),
133            crate::charlie(),
134            crate::dave(),
135            crate::eve(),
136            crate::ferdie(),
137            crate::one(),
138            crate::two(),
139        ]
140        .map(|kp| kp.public_key().0)
141        .map(From::from);
142        for account in accounts.iter() {
143            sandbox
144                .mint_into(account, TOKENS.into())
145                .unwrap_or_else(|_| panic!("Failed to mint {} tokens", TOKENS));
146        }
147    }
148}
149
150#[async_trait]
151impl<AccountId: AsRef<[u8; 32]> + Send, S: Sandbox> ChainBackend for Client<AccountId, S>
152where
153    S::Runtime: pallet_balances::Config,
154    AccountIdFor<S::Runtime>: From<[u8; 32]>,
155{
156    type AccountId = AccountId;
157    type Balance = BalanceOf<S::Runtime>;
158    type Error = SandboxErr;
159    type EventLog = ();
160
161    async fn create_and_fund_account(
162        &mut self,
163        _origin: &Keypair,
164        amount: Self::Balance,
165    ) -> Keypair {
166        let (pair, seed) = Pair::generate();
167
168        self.sandbox
169            .mint_into(&pair.public().0.into(), amount)
170            .expect("Failed to mint tokens");
171
172        Keypair::from_secret_key(seed).expect("Failed to create keypair")
173    }
174
175    async fn free_balance(
176        &mut self,
177        account: Self::AccountId,
178    ) -> Result<Self::Balance, Self::Error> {
179        let account = AccountIdFor::<S::Runtime>::from(*account.as_ref());
180        Ok(self.sandbox.free_balance(&account))
181    }
182
183    async fn runtime_call<'a>(
184        &mut self,
185        origin: &Keypair,
186        pallet_name: &'a str,
187        call_name: &'a str,
188        call_data: Vec<Value>,
189    ) -> Result<Self::EventLog, Self::Error> {
190        // Since in general, `ChainBackend::runtime_call` must be dynamic, we have to
191        // perform some translation here in order to invoke strongly-typed
192        // [`ink_sandbox::Sandbox`] API.
193
194        // Get metadata of the Sandbox runtime, so that we can encode the call object.
195        // Panic on error - metadata of the static im-memory runtime should always be
196        // available.
197        let raw_metadata: Vec<u8> = S::get_metadata().into();
198        let metadata = subxt_metadata::Metadata::decode(&mut raw_metadata.as_slice())
199            .expect("Failed to decode metadata");
200
201        // Encode the call object.
202        let call = subxt::dynamic::tx(pallet_name, call_name, call_data);
203        let encoded_call = call.encode_call_data(&metadata.into()).map_err(|err| {
204            SandboxErr::new(format!("runtime_call: Error encoding call: {err:?}"))
205        })?;
206
207        // Decode the call object.
208        // Panic on error - we just encoded a validated call object, so it should be
209        // decodable.
210        let decoded_call =
211            RuntimeCall::<S::Runtime>::decode(&mut encoded_call.as_slice())
212                .expect("Failed to decode runtime call");
213
214        // Execute the call.
215        self.sandbox
216            .runtime_call(
217                decoded_call,
218                S::convert_account_to_origin(keypair_to_account(origin)),
219            )
220            .map_err(|err| {
221                SandboxErr::new(format!("runtime_call: execution error {:?}", err.error))
222            })?;
223
224        Ok(())
225    }
226}
227
228#[async_trait]
229impl<
230        AccountId: Clone + Send + Sync + From<[u8; 32]> + AsRef<[u8; 32]>,
231        S: Sandbox,
232        E: Environment<AccountId = AccountId, Balance = ContractsBalanceOf<S::Runtime>>
233            + 'static,
234    > BuilderClient<E> for Client<AccountId, S>
235where
236    S::Runtime: pallet_balances::Config + pallet_revive::Config,
237    AccountIdFor<S::Runtime>: From<[u8; 32]> + AsRef<[u8; 32]>,
238    ContractsBalanceOf<S::Runtime>: Send + Sync,
239
240    ContractsBalanceOf<S::Runtime>: Into<U256> + TryFrom<U256> + Bounded,
241    MomentOf<S::Runtime>: Into<U256>,
242
243    // todo
244    <<S as Sandbox>::Runtime as frame_system::Config>::Hash:
245        frame_support::traits::IsType<sp_core::H256>,
246{
247    async fn bare_instantiate<
248        Contract: Clone,
249        Args: Send + Sync + AbiEncodeWith<Abi> + Clone,
250        R,
251        Abi: Send + Sync + Clone,
252    >(
253        &mut self,
254        contract_name: &str,
255        caller: &Keypair,
256        constructor: &mut CreateBuilderPartial<E, Contract, Args, R, Abi>,
257        value: E::Balance,
258        gas_limit: Weight,
259        storage_deposit_limit: DepositLimit<E::Balance>,
260    ) -> Result<BareInstantiationResult<Self::EventLog>, Self::Error> {
261        let _ =
262            <Client<AccountId, S> as BuilderClient<E>>::map_account(self, caller).await;
263
264        // get the trace
265        let _ = self.sandbox.build_block();
266
267        let tracer_config = TracerConfig::CallTracer { with_logs: true };
268
269        let mut tracer =
270            tracer_config.build(pallet_revive::Pallet::<S::Runtime>::evm_gas_from_weight);
271        let result = pallet_revive::tracing::trace(&mut tracer, || {
272            let code = self.contracts.load_code(contract_name);
273            let data = constructor_exec_input(constructor.clone());
274            self.sandbox.deploy_contract(
275                code,
276                value,
277                data,
278                salt(),
279                caller_to_origin::<S>(caller),
280                gas_limit,
281                storage_deposit_limit,
282            )
283        });
284
285        let addr_raw = match &result.result {
286            Err(err) => {
287                log_error(&format!("Instantiation failed: {err:?}"));
288                return Err(SandboxErr::new(format!("bare_instantiate: {err:?}")));
289            }
290            Ok(res) => res.addr,
291        };
292
293        let mut traces = tracer.collect_traces();
294        assert_eq!(traces.len(), 1);
295        let trace = traces.pop();
296
297        Ok(BareInstantiationResult {
298            addr: addr_raw,
299            events: (), // todo: https://github.com/Cardinal-Cryptography/drink/issues/32
300            trace,
301        })
302    }
303
304    async fn bare_instantiate_dry_run<
305        Contract: Clone,
306        Args: Send + Sync + AbiEncodeWith<Abi> + Clone,
307        R,
308        Abi: Send + Sync + Clone,
309    >(
310        &mut self,
311        contract_name: &str,
312        caller: &Keypair,
313        constructor: &mut CreateBuilderPartial<E, Contract, Args, R, Abi>,
314        value: E::Balance,
315        storage_deposit_limit: DepositLimit<E::Balance>,
316    ) -> Result<InstantiateDryRunResult<E>, Self::Error> {
317        // todo has to be: let _ = <Client<AccountId, S> as
318        // BuilderClient<E>>::map_account_dry_run(self, &caller).await;
319        let _ =
320            <Client<AccountId, S> as BuilderClient<E>>::map_account(self, caller).await;
321
322        let code = self.contracts.load_code(contract_name);
323        let data = constructor_exec_input(constructor.clone());
324        let result = self.sandbox.dry_run(|sandbox| {
325            sandbox.deploy_contract(
326                code,
327                value,
328                data,
329                salt(),
330                caller_to_origin::<S>(caller),
331                S::default_gas_limit(),
332                storage_deposit_limit,
333            )
334        });
335
336        let addr_id_raw = match &result.result {
337            Err(err) => {
338                panic!("Instantiate dry-run failed: {err:?}!")
339            }
340            Ok(res) => res.addr,
341        };
342
343        let result = ContractResult::<InstantiateReturnValue, E::Balance> {
344            gas_consumed: result.gas_consumed,
345            gas_required: result.gas_required,
346            storage_deposit: result.storage_deposit,
347            result: result.result.map(|r| {
348                InstantiateReturnValue {
349                    result: r.result,
350                    addr: addr_id_raw, // todo
351                }
352            }),
353        };
354        Ok(result.into())
355    }
356
357    async fn bare_upload(
358        &mut self,
359        contract_name: &str,
360        caller: &Keypair,
361        storage_deposit_limit: E::Balance,
362    ) -> Result<UploadResult<E, Self::EventLog>, Self::Error> {
363        let code = self.contracts.load_code(contract_name);
364
365        let result = match self.sandbox.upload_contract(
366            code,
367            caller_to_origin::<S>(caller),
368            storage_deposit_limit,
369        ) {
370            Ok(result) => result,
371            Err(err) => {
372                log_error(&format!("Upload failed: {err:?}"));
373                return Err(SandboxErr::new(format!("bare_upload: {err:?}")))
374            }
375        };
376
377        Ok(UploadResult {
378            code_hash: result.code_hash,
379            dry_run: Ok(CodeUploadReturnValue {
380                code_hash: result.code_hash,
381                deposit: result.deposit,
382            }),
383            events: (),
384        })
385    }
386
387    async fn bare_remove_code(
388        &mut self,
389        _caller: &Keypair,
390        _code_hash: H256,
391    ) -> Result<Self::EventLog, Self::Error> {
392        unimplemented!("sandbox does not yet support remove_code")
393    }
394
395    async fn bare_call<
396        Args: Sync + AbiEncodeWith<Abi> + Clone,
397        RetType: Send + AbiDecodeWith<Abi>,
398        Abi: Sync + Clone,
399    >(
400        &mut self,
401        caller: &Keypair,
402        message: &CallBuilderFinal<E, Args, RetType, Abi>,
403        value: E::Balance,
404        gas_limit: Weight,
405        storage_deposit_limit: DepositLimit<E::Balance>,
406    ) -> Result<(Self::EventLog, Option<CallTrace>), Self::Error>
407    where
408        CallBuilderFinal<E, Args, RetType, Abi>: Clone,
409    {
410        let _ =
411            <Client<AccountId, S> as BuilderClient<E>>::map_account(self, caller).await;
412
413        // todo rename any account_id coming back from callee
414        let addr = *message.clone().params().callee();
415        let exec_input = message.clone().params().exec_input().encode();
416
417        // todo
418        let tracer_config = TracerConfig::CallTracer { with_logs: true };
419        let mut tracer =
420            tracer_config.build(pallet_revive::Pallet::<S::Runtime>::evm_gas_from_weight);
421        let _result = pallet_revive::tracing::trace(&mut tracer, || {
422            self.sandbox
423                .call_contract(
424                    addr,
425                    value,
426                    exec_input,
427                    caller_to_origin::<S>(caller),
428                    gas_limit,
429                    storage_deposit_limit,
430                )
431                .result
432                .map_err(|err| SandboxErr::new(format!("bare_call: {err:?}")))
433        })?;
434        let mut traces = tracer.collect_traces();
435        assert_eq!(traces.len(), 1);
436        let trace = traces.pop();
437
438        Ok(((), trace))
439    }
440
441    async fn bare_call_dry_run<
442        Args: Sync + AbiEncodeWith<Abi> + Clone,
443        RetType: Send + AbiDecodeWith<Abi>,
444        Abi: Sync + Clone,
445    >(
446        &mut self,
447        caller: &Keypair,
448        message: &CallBuilderFinal<E, Args, RetType, Abi>,
449        value: E::Balance,
450        storage_deposit_limit: DepositLimit<E::Balance>,
451    ) -> Result<CallDryRunResult<E, RetType>, Self::Error>
452    where
453        CallBuilderFinal<E, Args, RetType, Abi>: Clone,
454    {
455        // todo there's side effects here
456        let _ =
457            <Client<AccountId, S> as BuilderClient<E>>::map_account(self, caller).await;
458
459        let addr = *message.clone().params().callee();
460        let exec_input = message.clone().params().exec_input().encode();
461
462        let result = self.sandbox.dry_run(|sandbox| {
463            sandbox.call_contract(
464                addr,
465                value,
466                exec_input,
467                caller_to_origin::<S>(caller),
468                S::default_gas_limit(),
469                storage_deposit_limit,
470            )
471        });
472        if result.result.is_err() {
473            let res = result.result.clone().unwrap_err();
474            if let DispatchError::Module(m) = res {
475                let msg = m.message;
476                if msg.is_some() {
477                    let s = msg.unwrap();
478                    if s.contains("AccountUnmapped") {
479                        panic!("something is wrong, we mapped the account before")
480                    }
481                }
482            }
483        }
484        // todo error when `AccountUnmapped`
485        Ok(CallDryRunResult {
486            exec_result: ContractExecResultFor::<E> {
487                gas_consumed: result.gas_consumed,
488                gas_required: result.gas_required,
489                storage_deposit: result.storage_deposit,
490                result: result.result,
491            },
492            _marker: Default::default(),
493            trace: None, // todo
494        })
495    }
496
497    async fn map_account_dry_run(&mut self, caller: &Keypair) -> Result<(), Self::Error> {
498        let caller = keypair_to_account(caller);
499        let origin = RawOrigin::Signed(caller);
500        let origin = OriginFor::<S::Runtime>::from(origin);
501
502        self.sandbox
503            .dry_run(|sandbox| sandbox.map_account(origin))
504            .map_err(|err| {
505                SandboxErr::new(format!("map_account_dry_run: execution error {:?}", err))
506            })
507    }
508
509    async fn map_account(&mut self, caller: &Keypair) -> Result<(), Self::Error> {
510        let caller = keypair_to_account(caller);
511        let origin = RawOrigin::Signed(caller);
512        let origin = OriginFor::<S::Runtime>::from(origin);
513
514        self.sandbox.map_account(origin).map_err(|err| {
515            SandboxErr::new(format!("map_account: execution error {:?}", err))
516        })
517    }
518}
519
520impl<
521        AccountId: Clone + Send + Sync + From<[u8; 32]> + AsRef<[u8; 32]>,
522        Config: Sandbox,
523        E: Environment<
524                AccountId = AccountId,
525                Balance = ContractsBalanceOf<Config::Runtime>,
526            > + 'static,
527    > E2EBackend<E> for Client<AccountId, Config>
528where
529    Config::Runtime: pallet_balances::Config + pallet_revive::Config,
530    AccountIdFor<Config::Runtime>: From<[u8; 32]> + AsRef<[u8; 32]>,
531    ContractsBalanceOf<Config::Runtime>: Send + Sync,
532    ContractsBalanceOf<Config::Runtime>: Into<U256> + TryFrom<U256> + Bounded,
533    MomentOf<Config::Runtime>: Into<U256>,
534
535    // todo
536    <Config::Runtime as frame_system::Config>::Hash: IsType<sp_core::H256>,
537{
538}
539
540fn keypair_to_account<AccountId: From<[u8; 32]>>(keypair: &Keypair) -> AccountId {
541    AccountId::from(keypair.public_key().0)
542}
543
544fn caller_to_origin<S>(caller: &Keypair) -> OriginFor<S::Runtime>
545where
546    S: Sandbox,
547    S::Runtime: pallet_balances::Config + pallet_revive::Config,
548    AccountIdFor<S::Runtime>: From<[u8; 32]> + AsRef<[u8; 32]>,
549{
550    let caller = keypair_to_account(caller);
551    let origin = RawOrigin::Signed(caller);
552    OriginFor::<S::Runtime>::from(origin)
553}
554
555#[async_trait]
556impl<
557        AccountId: Clone + Send + Sync + From<[u8; 32]> + AsRef<[u8; 32]>,
558        S: Sandbox,
559        E: Environment<AccountId = AccountId, Balance = ContractsBalanceOf<S::Runtime>>
560            + 'static,
561    > ContractsBackend<E> for Client<AccountId, S>
562where
563    S::Runtime: pallet_balances::Config + pallet_revive::Config,
564    AccountIdFor<S::Runtime>: From<[u8; 32]> + AsRef<[u8; 32]>,
565{
566    type Error = SandboxErr;
567    type EventLog = ();
568}
569
570/// Exposes preset sandbox configurations to be used in tests.
571pub mod preset {
572    pub mod mock_network {
573        use ink_sandbox::{
574            frame_system,
575            AccountIdFor,
576            BlockBuilder,
577            Extension,
578            RuntimeMetadataPrefixed,
579            Sandbox,
580            Snapshot,
581        };
582        pub use pallet_revive_mock_network::*;
583        use sp_runtime::traits::Dispatchable;
584
585        /// A [`ink_sandbox::Sandbox`] that can be used to test contracts
586        /// with a mock network of relay chain and parachains.
587        ///
588        /// ```no_compile
589        /// #[ink_e2e::test(backend(runtime_only(sandbox = MockNetworkSandbox)))]
590        /// async fn my_test<Client: E2EBackend>(mut client: Client) -> E2EResult<()> {
591        ///   // ...
592        /// }
593        /// ```
594        #[derive(Default)]
595        pub struct MockNetworkSandbox {
596            dry_run: bool,
597        }
598
599        impl Sandbox for MockNetworkSandbox {
600            type Runtime = parachain::Runtime;
601
602            fn execute_with<T>(&mut self, execute: impl FnOnce() -> T) -> T {
603                if self.dry_run {
604                    ParaA::execute_with(execute)
605                } else {
606                    ParaA::execute_without_dispatch(execute)
607                }
608            }
609
610            fn dry_run<T>(&mut self, action: impl FnOnce(&mut Self) -> T) -> T {
611                EXT_PARAA.with(|v| {
612                    let backend_backup = v.borrow_mut().as_backend();
613                    self.dry_run = true;
614                    let result = action(self);
615                    self.dry_run = false;
616
617                    let mut v = v.borrow_mut();
618                    v.commit_all().expect("Failed to commit changes");
619                    v.backend = backend_backup;
620                    result
621                })
622            }
623
624            fn register_extension<E: ::core::any::Any + Extension>(&mut self, ext: E) {
625                EXT_PARAA.with(|v| v.borrow_mut().register_extension(ext));
626            }
627
628            fn initialize_block(
629                height: frame_system::pallet_prelude::BlockNumberFor<Self::Runtime>,
630                parent_hash: <Self::Runtime as frame_system::Config>::Hash,
631            ) {
632                BlockBuilder::<Self::Runtime>::initialize_block(height, parent_hash)
633            }
634
635            fn finalize_block(
636                height: frame_system::pallet_prelude::BlockNumberFor<Self::Runtime>,
637            ) -> <Self::Runtime as frame_system::Config>::Hash {
638                BlockBuilder::<Self::Runtime>::finalize_block(height)
639            }
640
641            fn default_actor() -> AccountIdFor<Self::Runtime> {
642                ALICE
643            }
644
645            fn get_metadata() -> RuntimeMetadataPrefixed {
646                parachain::Runtime::metadata()
647            }
648
649            fn convert_account_to_origin(
650                account: AccountIdFor<Self::Runtime>,
651            ) -> <<Self::Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin
652            {
653                Some(account).into()
654            }
655
656            fn take_snapshot(&mut self) -> Snapshot {
657                EXT_PARAA.with(|v| {
658                    let mut v = v.borrow_mut();
659                    let mut backend = v.as_backend().clone();
660                    let raw_key_values = backend
661                        .backend_storage_mut()
662                        .drain()
663                        .into_iter()
664                        .filter(|(_, (_, r))| *r > 0)
665                        .collect::<Vec<(Vec<u8>, (Vec<u8>, i32))>>();
666                    let root = backend.root().to_owned();
667
668                    Snapshot {
669                        storage: raw_key_values,
670                        storage_root: root,
671                    }
672                })
673            }
674
675            fn restore_snapshot(&mut self, snapshot: ink_sandbox::Snapshot) {
676                EXT_PARAA.with(|v| {
677                    let mut v = v.borrow_mut();
678
679                    *v = ink_sandbox::TestExternalities::from_raw_snapshot(
680                        snapshot.storage,
681                        snapshot.storage_root,
682                        Default::default(),
683                    );
684                })
685            }
686        }
687    }
688}