ink_sandbox/
macros.rs

1use std::time::SystemTime;
2
3use frame_support::{
4    sp_runtime::{
5        BuildStorage,
6        traits::{
7            Header,
8            One,
9        },
10    },
11    traits::Hooks,
12};
13use frame_system::pallet_prelude::BlockNumberFor;
14use sp_io::TestExternalities;
15
16/// Asserts that a contract call succeeded without reverting.
17///
18/// This macro follows FRAME's `assert_ok!` convention for consistency across
19/// the Polkadot ecosystem. It verifies that a contract call completed successfully
20/// and did not revert. If the call reverted, the macro panics with a detailed
21/// error message extracted from the call trace.
22///
23/// # Behavior
24///
25/// - Takes a `CallResult` as input
26/// - Checks if `dry_run.did_revert()` is `false`
27/// - Panics with error details if the call reverted
28/// - Returns the `CallResult` for further inspection if successful
29///
30/// # Examples
31///
32/// ```ignore
33/// let result = client.call(&alice, &transfer).submit().await?;
34/// assert_ok!(result);
35/// ```
36#[macro_export]
37macro_rules! assert_ok {
38    ($result:expr) => {{
39        let result = $result;
40        if result.dry_run.did_revert() {
41            panic!(
42                "Expected call to succeed but it reverted.\nError: {:?}",
43                result.extract_error()
44            );
45        }
46        result
47    }};
48    ($result:expr, $($msg:tt)+) => {{
49        let result = $result;
50        if result.dry_run.did_revert() {
51            panic!(
52                "{}\nExpected call to succeed but it reverted.\nError: {:?}",
53                format_args!($($msg)+),
54                result.extract_error()
55            );
56        }
57        result
58    }};
59}
60
61/// Asserts that a contract call reverted with a specific error.
62///
63/// This macro follows FRAME's `assert_noop!` convention, which stands for
64/// "assert no operation" - meaning the call should fail without changing state.
65/// It verifies that a contract call reverted and that the revert reason matches
66/// the expected error string.
67///
68/// # Behavior
69///
70/// - Takes a `CallResult` and an expected error string as input
71/// - Checks if `dry_run.did_revert()` is `true`
72/// - Panics if the call succeeded (did not revert)
73/// - Extracts the error from the call trace using `extract_error()`
74/// - Panics if the actual error doesn't match the expected error
75/// - Returns the `CallResult` if both checks pass
76///
77/// # Examples
78///
79/// ```ignore
80/// let result = client.call(&alice, &insufficient_transfer).submit().await?;
81/// assert_noop!(result, "BalanceLow");
82/// ```
83#[macro_export]
84macro_rules! assert_noop {
85    ($result:expr, $expected_error:expr) => {{
86        let result = $result;
87        if !result.dry_run.did_revert() {
88            panic!(
89                "Expected call to revert with '{}' but it succeeded",
90                $expected_error
91            );
92        }
93
94        let actual_error = result.extract_error();
95        if actual_error != Some($expected_error.to_string()) {
96            panic!(
97                "Expected error '{}' but got {:?}",
98                $expected_error,
99                actual_error
100            );
101        }
102
103        result
104    }};
105    ($result:expr, $expected_error:expr, $($msg:tt)+) => {{
106        let result = $result;
107        if !result.dry_run.did_revert() {
108            panic!(
109                "{}\nExpected call to revert with '{}' but it succeeded",
110                format_args!($($msg)+),
111                $expected_error
112            );
113        }
114
115        let actual_error = result.extract_error();
116        if actual_error != Some($expected_error.to_string()) {
117            panic!(
118                "{}\nExpected error '{}' but got {:?}",
119                format_args!($($msg)+),
120                $expected_error,
121                actual_error
122            );
123        }
124
125        result
126    }};
127}
128
129/// Asserts that the latest contract event matches an expected event.
130///
131/// This macro verifies that the last emitted contract event from the sandbox
132/// matches the provided expected event.
133///
134/// # Parameters
135/// - `client` - Mutable reference to the sandbox client
136/// - `event` - The expected event
137#[macro_export]
138macro_rules! assert_last_event {
139    ($client:expr, $event:expr $(,)?) => {
140        $crate::client::assert_last_contract_event_inner($client, $event)
141    };
142}
143
144/// A helper struct for initializing and finalizing blocks.
145pub struct BlockBuilder<T>(std::marker::PhantomData<T>);
146
147impl<
148    T: pallet_balances::Config
149        + pallet_timestamp::Config<Moment = u64>
150        + pallet_revive::Config,
151> BlockBuilder<T>
152{
153    /// Create a new externalities with the given balances.
154    pub fn new_ext(
155        balances: Vec<(T::AccountId, <T as pallet_balances::Config>::Balance)>,
156    ) -> TestExternalities {
157        let mut storage = frame_system::GenesisConfig::<T>::default()
158            .build_storage()
159            .unwrap();
160
161        pallet_balances::GenesisConfig::<T> {
162            balances,
163            dev_accounts: None,
164        }
165        .assimilate_storage(&mut storage)
166        .unwrap();
167
168        let mut ext = TestExternalities::new(storage);
169
170        ext.execute_with(|| {
171            Self::initialize_block(BlockNumberFor::<T>::one(), Default::default())
172        });
173        ext
174    }
175
176    /// Initialize a new block at particular height.
177    pub fn initialize_block(
178        height: frame_system::pallet_prelude::BlockNumberFor<T>,
179        parent_hash: <T as frame_system::Config>::Hash,
180    ) {
181        frame_system::Pallet::<T>::reset_events();
182        frame_system::Pallet::<T>::initialize(&height, &parent_hash, &Default::default());
183        pallet_balances::Pallet::<T>::on_initialize(height);
184        pallet_timestamp::Pallet::<T>::set_timestamp(
185            SystemTime::now()
186                .duration_since(SystemTime::UNIX_EPOCH)
187                .expect("Time went backwards")
188                .as_secs(),
189        );
190        pallet_timestamp::Pallet::<T>::on_initialize(height);
191        pallet_revive::Pallet::<T>::on_initialize(height);
192        frame_system::Pallet::<T>::note_finished_initialize();
193    }
194
195    /// Finalize a block at particular height.
196    pub fn finalize_block(
197        height: frame_system::pallet_prelude::BlockNumberFor<T>,
198    ) -> <T as frame_system::Config>::Hash {
199        pallet_revive::Pallet::<T>::on_finalize(height);
200        use sp_core::Get;
201        let minimum_period = <T as pallet_timestamp::Config>::MinimumPeriod::get();
202        let now = pallet_timestamp::Pallet::<T>::get()
203            .checked_add(minimum_period)
204            .unwrap();
205        pallet_timestamp::Pallet::<T>::set_timestamp(now);
206        pallet_timestamp::Pallet::<T>::on_finalize(height);
207        pallet_balances::Pallet::<T>::on_finalize(height);
208        frame_system::Pallet::<T>::finalize().hash()
209    }
210}
211
212/// Macro creating a minimal runtime with the given name.
213///
214/// The new macro will automatically implement `crate::Sandbox`.
215#[macro_export]
216macro_rules! create_sandbox {
217    ($name:ident) => {
218        $crate::paste::paste! {
219            $crate::create_sandbox!($name, [<$name Runtime>], (), {});
220        }
221    };
222    ($name:ident, $debug: ty) => {
223        $crate::paste::paste! {
224            $crate::create_sandbox!($name, [<$name Runtime>], $debug, {});
225        }
226    };
227    ($name:ident, $debug: ty, { $( $pallet_name:tt : $pallet:ident ),* $(,)? }) => {
228        $crate::paste::paste! {
229            $crate::create_sandbox!($name, [<$name Runtime>], $debug, {
230                $(
231                    $pallet_name : $pallet,
232                )*
233            });
234        }
235    };
236    ($sandbox:ident, $runtime:ident, $debug: ty, { $( $pallet_name:tt : $pallet:ident ),* $(,)? }) => {
237
238// Put all the boilerplate into an auxiliary module
239mod construct_runtime {
240
241    // Bring some common types into the scope
242    use $crate::frame_support::{
243        construct_runtime,
244        derive_impl,
245        parameter_types,
246        sp_runtime::{
247            traits::Convert,
248            AccountId32, Perbill,
249            FixedU128,
250        },
251        traits::{ConstBool, ConstU8, ConstU128, ConstU32, ConstU64, Currency},
252        weights::{Weight, IdentityFee},
253    };
254
255    use $crate::pallet_transaction_payment::{FungibleAdapter};
256
257    use $crate::Snapshot;
258
259    pub type Balance = u128;
260
261    // Define the runtime type as a collection of pallets
262    construct_runtime!(
263        pub enum $runtime {
264            System: $crate::frame_system,
265            Balances: $crate::pallet_balances,
266            Timestamp: $crate::pallet_timestamp,
267            Assets: $crate::pallet_assets::<Instance1>,
268            Revive: $crate::pallet_revive,
269            TransactionPayment: $crate::pallet_transaction_payment,
270            $(
271                $pallet_name: $pallet,
272            )*
273        }
274    );
275
276    #[derive_impl($crate::frame_system::config_preludes::SolochainDefaultConfig as $crate::frame_system::DefaultConfig)]
277    impl $crate::frame_system::Config for $runtime {
278        type Block = $crate::frame_system::mocking::MockBlockU32<$runtime>;
279        type Version = ();
280        type BlockHashCount = ConstU32<250>;
281        type AccountData = $crate::pallet_balances::AccountData<<$runtime as $crate::pallet_balances::Config>::Balance>;
282    }
283
284    impl $crate::pallet_balances::Config for $runtime {
285        type RuntimeEvent = RuntimeEvent;
286        type WeightInfo = ();
287        type Balance = Balance;
288        type DustRemoval = ();
289        type ExistentialDeposit = ConstU128<1>;
290        type AccountStore = System;
291        type ReserveIdentifier = [u8; 8];
292        type FreezeIdentifier = ();
293        type MaxLocks = ();
294        type MaxReserves = ();
295        type MaxFreezes = ();
296        type RuntimeHoldReason = RuntimeHoldReason;
297        type RuntimeFreezeReason = RuntimeFreezeReason;
298        type DoneSlashHandler = ();
299    }
300
301    impl $crate::pallet_timestamp::Config for $runtime {
302        type Moment = u64;
303        type OnTimestampSet = ();
304        type MinimumPeriod = ConstU64<1>;
305        type WeightInfo = ();
306    }
307
308    // Configure pallet-assets (Instance1 for Trust Backed Assets)
309    pub type TrustBackedAssetsInstance = $crate::pallet_assets::Instance1;
310    pub type AssetIdForTrustBackedAssets = u32;
311
312    impl $crate::pallet_assets::Config<TrustBackedAssetsInstance> for $runtime {
313        type RuntimeEvent = RuntimeEvent;
314        type Balance = Balance;
315        type AssetId = AssetIdForTrustBackedAssets;
316        type AssetIdParameter = $crate::scale::Compact<AssetIdForTrustBackedAssets>;
317        type Currency = Balances;
318        type CreateOrigin = $crate::frame_support::traits::AsEnsureOriginWithArg<$crate::frame_system::EnsureSigned<AccountId32>>;
319        type ForceOrigin = $crate::frame_system::EnsureRoot<AccountId32>;
320        type AssetDeposit = ConstU128<1>;
321        type AssetAccountDeposit = ConstU128<1>;
322        type MetadataDepositBase = ConstU128<1>;
323        type MetadataDepositPerByte = ConstU128<1>;
324        type ApprovalDeposit = ConstU128<1>;
325        type StringLimit = ConstU32<50>;
326        type Freezer = ();
327        type Holder = ();
328        type Extra = ();
329        type WeightInfo = ();
330        type CallbackHandle = $crate::pallet_assets::AutoIncAssetId<$runtime, TrustBackedAssetsInstance>;
331        type RemoveItemsLimit = ConstU32<1000>;
332    }
333
334    impl $crate::pallet_transaction_payment::Config for $runtime {
335        type RuntimeEvent = RuntimeEvent;
336        type OnChargeTransaction = FungibleAdapter<Balances, ()>;
337        type OperationalFeeMultiplier = ConstU8<5>;
338        type WeightToFee = $crate::pallet_revive::evm::fees::BlockRatioFee<1, 1, Self>;
339        type LengthToFee = IdentityFee<Balance>;
340        type FeeMultiplierUpdate = ();
341        type WeightInfo = $crate::pallet_transaction_payment::weights::SubstrateWeight<$runtime>;
342    }
343
344    // Configure `pallet-revive`
345    type BalanceOf = <Balances as Currency<AccountId32>>::Balance;
346    impl Convert<Weight, BalanceOf> for $runtime {
347        fn convert(w: Weight) -> BalanceOf {
348            w.ref_time().into()
349        }
350    }
351
352    // Unit = the base number of indivisible units for balances
353    const UNIT: Balance = 1_000_000_000_000;
354    const MILLIUNIT: Balance = 1_000_000_000;
355
356    const fn deposit(items: u32, bytes: u32) -> Balance {
357        (items as Balance * UNIT + (bytes as Balance) * (5 * MILLIUNIT / 100)) / 10
358    }
359
360    parameter_types! {
361        pub CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0);
362        pub const MaxEthExtrinsicWeight: FixedU128 = FixedU128::from_rational(1,2);
363        pub const DepositPerChildTrieItem: Balance = deposit(1, 0) / 100;
364    }
365
366    impl $crate::pallet_revive::Config for $runtime {
367        type AddressMapper = $crate::pallet_revive::AccountId32Mapper<Self>;
368        type ChainId = ConstU64<1>;
369        type NativeToEthRatio = ConstU32<100_000_000>;
370        type Time = Timestamp;
371        type Balance = Balance;
372        type Currency = Balances;
373        type RuntimeEvent = RuntimeEvent;
374        type RuntimeCall = RuntimeCall;
375        type RuntimeOrigin = RuntimeOrigin;
376        type DepositPerItem = ConstU128<1>;
377        type DepositPerChildTrieItem = DepositPerChildTrieItem;
378        type DepositPerByte = ConstU128<1>;
379        type WeightInfo = ();
380        type RuntimeMemory = ConstU32<{ 128 * 1024 * 1024 }>;
381        type PVFMemory = ConstU32<{ 512 * 1024 * 1024 }>;
382        type UnsafeUnstableInterface = ConstBool<true>;
383        type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent;
384        type RuntimeHoldReason = RuntimeHoldReason;
385        type UploadOrigin = $crate::frame_system::EnsureSigned<Self::AccountId>;
386        type InstantiateOrigin = $crate::frame_system::EnsureSigned<Self::AccountId>;
387        type FindAuthor = ();
388        type Precompiles = (
389            $crate::pallet_assets_precompiles::ERC20<Self, $crate::pallet_assets_precompiles::InlineIdConfig<{ 0x0120 }>, TrustBackedAssetsInstance>,
390            // todo add `PoolAssetsInstance`
391        );
392        type AllowEVMBytecode = ConstBool<false>;
393        type FeeInfo = ();
394        type MaxEthExtrinsicWeight = MaxEthExtrinsicWeight;
395        type DebugEnabled = ConstBool<false>;
396    }
397
398    /// Default initial balance for the default account.
399    pub const INITIAL_BALANCE: u128 = 1_000_000_000_000_000;
400    pub const DEFAULT_ACCOUNT: AccountId32 = AccountId32::new([1u8; 32]);
401
402    pub struct $sandbox {
403        ext: $crate::TestExternalities,
404    }
405
406    impl ::std::default::Default for $sandbox {
407        fn default() -> Self {
408            let ext = $crate::macros::BlockBuilder::<$runtime>::new_ext(vec![(
409                DEFAULT_ACCOUNT,
410                INITIAL_BALANCE,
411            )]);
412            Self { ext }
413        }
414    }
415
416    // Implement `crate::Sandbox` trait
417    impl $crate::Sandbox for $sandbox {
418        type Runtime = $runtime;
419
420        fn execute_with<T>(&mut self, execute: impl FnOnce() -> T) -> T {
421            self.ext.execute_with(execute)
422        }
423
424        fn dry_run<T>(&mut self, action: impl FnOnce(&mut Self) -> T) -> T {
425            // Make a backup of the backend.
426            let backend_backup = self.ext.as_backend();
427            // Run the action, potentially modifying storage. Ensure, that there are no pending changes
428            // that would affect the reverted backend.
429            let result = action(self);
430            self.ext.commit_all().expect("Failed to commit changes");
431
432            // Restore the backend.
433            self.ext.backend = backend_backup;
434            result
435        }
436
437        fn register_extension<E: ::core::any::Any + $crate::Extension>(&mut self, ext: E) {
438            self.ext.register_extension(ext);
439        }
440
441        fn initialize_block(
442            height: $crate::frame_system::pallet_prelude::BlockNumberFor<Self::Runtime>,
443            parent_hash: <Self::Runtime as $crate::frame_system::Config>::Hash,
444        ) {
445            $crate::macros::BlockBuilder::<Self::Runtime>::initialize_block(height, parent_hash)
446        }
447
448        fn finalize_block(
449            height: $crate::frame_system::pallet_prelude::BlockNumberFor<Self::Runtime>,
450        ) -> <Self::Runtime as $crate::frame_system::Config>::Hash {
451            $crate::macros::BlockBuilder::<Self::Runtime>::finalize_block(height)
452        }
453
454        fn default_actor() -> $crate::AccountIdFor<Self::Runtime> {
455            DEFAULT_ACCOUNT
456        }
457
458        fn get_metadata() -> $crate::RuntimeMetadataPrefixed {
459            Self::Runtime::metadata()
460        }
461
462        fn convert_account_to_origin(
463            account: $crate::AccountIdFor<Self::Runtime>,
464        ) -> <<Self::Runtime as $crate::frame_system::Config>::RuntimeCall as $crate::frame_support::sp_runtime::traits::Dispatchable>::RuntimeOrigin {
465            Some(account).into()
466        }
467
468        fn take_snapshot(&mut self) -> Snapshot {
469            let mut backend = self.ext.as_backend().clone();
470            let raw_key_values = backend
471                .backend_storage_mut()
472                .drain()
473                .into_iter()
474                .filter(|(_, (_, r))| *r > 0)
475                .collect::<Vec<(Vec<u8>, (Vec<u8>, i32))>>();
476            let root = backend.root().to_owned();
477            Snapshot {
478                storage: raw_key_values,
479                storage_root: root,
480            }
481        }
482
483        fn restore_snapshot(&mut self, snapshot: Snapshot) {
484            self.ext = $crate::TestExternalities::from_raw_snapshot(
485                snapshot.storage,
486                snapshot.storage_root,
487                Default::default(),
488            );
489        }
490    }
491}
492
493// Export runtime type itself, pallets and useful types from the auxiliary module
494pub use construct_runtime::{
495    $sandbox, $runtime, Assets, AssetIdForTrustBackedAssets, Balances, Revive, PalletInfo,
496    RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, System, Timestamp,
497    TrustBackedAssetsInstance,
498};
499    };
500}
501
502create_sandbox!(DefaultSandbox);