fuel_vm/
util.rs

1//! FuelVM utilities
2
3/// A utility macro for writing scripts with the data offset included. Since the
4/// script data offset depends on the length of the script, this macro will
5/// evaluate the length and then rewrite the resultant script output with the
6/// correct offset (using the offset parameter).
7///
8/// # Example
9///
10/// ```
11/// use fuel_asm::{op, RegId};
12/// use fuel_types::{Immediate18, Word, canonical::Serialize};
13/// use fuel_vm::prelude::{Call, TxParameters, ContractId, Opcode};
14/// use fuel_vm::script_with_data_offset;
15/// use itertools::Itertools;
16///
17/// // Example of making a contract call script using script_data for the call info and asset id.
18/// let contract_id = ContractId::from([0x11; 32]);
19/// let call = Call::new(contract_id, 0, 0).to_bytes();
20/// let asset_id = [0x00; 32];
21/// let transfer_amount: Word = 100;
22/// let gas_to_forward = 100_000;
23/// let script_data = [call.as_ref(), asset_id.as_ref()]
24///     .into_iter()
25///     .flatten()
26///     .copied()
27///     .collect_vec();
28///
29/// // Use the macro since we don't know the exact offset for script_data.
30/// let (script, data_offset) = script_with_data_offset!(
31///     data_offset,
32///     vec![
33///         // use data_offset to reference the location of the call bytes inside script_data
34///         op::movi(0x10, data_offset),
35///         op::movi(0x11, transfer_amount as Immediate18),
36///         // use data_offset again to reference the location of the asset id inside of script data
37///         op::movi(0x12, data_offset + call.len() as Immediate18),
38///         op::movi(0x13, gas_to_forward as Immediate18),
39///         op::call(0x10, 0x11, 0x12, 0x13),
40///         op::ret(RegId::ONE),
41///     ],
42///     TxParameters::DEFAULT.tx_offset()
43/// );
44/// ```
45#[cfg(feature = "alloc")]
46#[macro_export]
47macro_rules! script_with_data_offset {
48    ($offset:ident, $script:expr, $tx_offset:expr) => {{
49        let $offset = {
50            // first set offset to 0 before evaluating script expression
51            let $offset = {
52                use $crate::prelude::Immediate18;
53                0 as Immediate18
54            };
55            // evaluate script expression with zeroed data offset to get the script length
56            let script_bytes: $crate::alloc::vec::Vec<u8> =
57                ::core::iter::IntoIterator::into_iter({ $script }).collect();
58            // compute the script data offset within the VM memory given the script length
59            {
60                use $crate::{
61                    fuel_tx::{
62                        field::Script as ScriptField,
63                        Script,
64                    },
65                    fuel_types::bytes::padded_len,
66                    prelude::Immediate18,
67                };
68                let value: Immediate18 = $tx_offset
69                    .saturating_add(Script::script_offset_static())
70                    .saturating_add(
71                        padded_len(script_bytes.as_slice()).unwrap_or(usize::MAX),
72                    )
73                    .try_into()
74                    .expect("script data offset is too large");
75                value
76            }
77        };
78        // re-evaluate and return the finalized script with the correct data offset length
79        // set.
80        ($script, $offset)
81    }};
82}
83
84#[allow(missing_docs)]
85#[cfg(feature = "random")]
86#[cfg(any(test, feature = "test-helpers"))]
87/// Testing utilities
88pub mod test_helpers {
89    use alloc::{
90        vec,
91        vec::Vec,
92    };
93
94    use crate::{
95        checked_transaction::{
96            builder::TransactionBuilderExt,
97            Checked,
98            IntoChecked,
99        },
100        interpreter::Memory,
101        memory_client::MemoryClient,
102        state::StateTransition,
103        storage::{
104            ContractsAssetsStorage,
105            MemoryStorage,
106        },
107        transactor::Transactor,
108    };
109    use anyhow::anyhow;
110
111    use crate::{
112        interpreter::{
113            CheckedMetadata,
114            ExecutableTransaction,
115            InterpreterParams,
116            MemoryInstance,
117        },
118        prelude::{
119            Backtrace,
120            Call,
121        },
122    };
123    use fuel_asm::{
124        op,
125        GTFArgs,
126        Instruction,
127        PanicReason,
128        RegId,
129    };
130    use fuel_tx::{
131        field::{
132            Outputs,
133            ReceiptsRoot,
134        },
135        BlobBody,
136        BlobIdExt,
137        ConsensusParameters,
138        Contract,
139        ContractParameters,
140        Create,
141        FeeParameters,
142        Finalizable,
143        GasCosts,
144        Input,
145        Output,
146        PredicateParameters,
147        Receipt,
148        Script,
149        ScriptParameters,
150        StorageSlot,
151        Transaction,
152        TransactionBuilder,
153        TxParameters,
154        Witness,
155    };
156    use fuel_types::{
157        canonical::{
158            Deserialize,
159            Serialize,
160        },
161        Address,
162        AssetId,
163        BlobId,
164        BlockHeight,
165        ChainId,
166        ContractId,
167        Immediate12,
168        Salt,
169        Word,
170    };
171    use itertools::Itertools;
172    use rand::{
173        prelude::StdRng,
174        Rng,
175        SeedableRng,
176    };
177
178    pub struct CreatedContract {
179        pub tx: Create,
180        pub contract_id: ContractId,
181        pub salt: Salt,
182    }
183
184    pub struct TestBuilder {
185        pub rng: StdRng,
186        gas_price: Word,
187        max_fee_limit: Word,
188        script_gas_limit: Word,
189        builder: TransactionBuilder<Script>,
190        storage: MemoryStorage,
191        block_height: BlockHeight,
192        consensus_params: ConsensusParameters,
193    }
194
195    impl TestBuilder {
196        pub fn new(seed: u64) -> Self {
197            let bytecode = core::iter::once(op::ret(RegId::ONE)).collect();
198            TestBuilder {
199                rng: StdRng::seed_from_u64(seed),
200                gas_price: 0,
201                max_fee_limit: 0,
202                script_gas_limit: 100,
203                builder: TransactionBuilder::script(bytecode, vec![]),
204                storage: MemoryStorage::default(),
205                block_height: Default::default(),
206                consensus_params: ConsensusParameters::standard(),
207            }
208        }
209
210        pub fn get_block_height(&self) -> BlockHeight {
211            self.block_height
212        }
213
214        pub fn start_script_bytes(
215            &mut self,
216            script: Vec<u8>,
217            script_data: Vec<u8>,
218        ) -> &mut Self {
219            self.start_script_inner(script, script_data)
220        }
221
222        pub fn start_script(
223            &mut self,
224            script: Vec<Instruction>,
225            script_data: Vec<u8>,
226        ) -> &mut Self {
227            let script = script.into_iter().collect();
228            self.start_script_inner(script, script_data)
229        }
230
231        fn start_script_inner(
232            &mut self,
233            script: Vec<u8>,
234            script_data: Vec<u8>,
235        ) -> &mut Self {
236            self.builder = TransactionBuilder::script(script, script_data);
237            self.builder.script_gas_limit(self.script_gas_limit);
238            self
239        }
240
241        pub fn gas_price(&mut self, price: Word) -> &mut TestBuilder {
242            self.gas_price = price;
243            self
244        }
245
246        pub fn max_fee_limit(&mut self, max_fee_limit: Word) -> &mut TestBuilder {
247            self.max_fee_limit = max_fee_limit;
248            self
249        }
250
251        pub fn script_gas_limit(&mut self, limit: Word) -> &mut TestBuilder {
252            self.builder.script_gas_limit(limit);
253            self.script_gas_limit = limit;
254            self
255        }
256
257        pub fn change_output(&mut self, asset_id: AssetId) -> &mut TestBuilder {
258            self.builder
259                .add_output(Output::change(self.rng.gen(), 0, asset_id));
260            self
261        }
262
263        pub fn coin_output(
264            &mut self,
265            asset_id: AssetId,
266            amount: Word,
267        ) -> &mut TestBuilder {
268            self.builder
269                .add_output(Output::coin(self.rng.gen(), amount, asset_id));
270            self
271        }
272
273        pub fn variable_output(&mut self, asset_id: AssetId) -> &mut TestBuilder {
274            self.builder
275                .add_output(Output::variable(Address::zeroed(), 0, asset_id));
276            self
277        }
278
279        pub fn contract_output(&mut self, id: &ContractId) -> &mut TestBuilder {
280            let input_idx = self
281                .builder
282                .inputs()
283                .iter()
284                .find_position(|input| matches!(input, Input::Contract(contract) if &contract.contract_id == id))
285                .expect("expected contract input with matching contract id");
286
287            self.builder.add_output(Output::contract(
288                u16::try_from(input_idx.0).expect("The input index is more than allowed"),
289                self.rng.gen(),
290                self.rng.gen(),
291            ));
292
293            self
294        }
295
296        pub fn coin_input(
297            &mut self,
298            asset_id: AssetId,
299            amount: Word,
300        ) -> &mut TestBuilder {
301            self.builder.add_unsigned_coin_input(
302                fuel_crypto::SecretKey::random(&mut self.rng),
303                self.rng.gen(),
304                amount,
305                asset_id,
306                Default::default(),
307            );
308            self
309        }
310
311        pub fn fee_input(&mut self) -> &mut TestBuilder {
312            self.builder.add_fee_input();
313            self
314        }
315
316        pub fn contract_input(&mut self, contract_id: ContractId) -> &mut TestBuilder {
317            self.builder.add_input(Input::contract(
318                self.rng.gen(),
319                self.rng.gen(),
320                self.rng.gen(),
321                self.rng.gen(),
322                contract_id,
323            ));
324            self
325        }
326
327        pub fn witness(&mut self, witness: Witness) -> &mut TestBuilder {
328            self.builder.add_witness(witness);
329            self
330        }
331
332        pub fn storage(&mut self, storage: MemoryStorage) -> &mut TestBuilder {
333            self.storage = storage;
334            self
335        }
336
337        pub fn block_height(&mut self, block_height: BlockHeight) -> &mut TestBuilder {
338            self.block_height = block_height;
339            self
340        }
341
342        pub fn with_fee_params(&mut self, fee_params: FeeParameters) -> &mut TestBuilder {
343            self.consensus_params.set_fee_params(fee_params);
344            self
345        }
346
347        pub fn with_free_gas_costs(&mut self) -> &mut TestBuilder {
348            let gas_costs = GasCosts::free();
349            self.consensus_params.set_gas_costs(gas_costs);
350            self
351        }
352
353        pub fn base_asset_id(&mut self, base_asset_id: AssetId) -> &mut TestBuilder {
354            self.consensus_params.set_base_asset_id(base_asset_id);
355            self
356        }
357
358        pub fn build(&mut self) -> Checked<Script> {
359            self.builder.max_fee_limit(self.max_fee_limit);
360            self.builder.with_tx_params(*self.get_tx_params());
361            self.builder
362                .with_contract_params(*self.get_contract_params());
363            self.builder
364                .with_predicate_params(*self.get_predicate_params());
365            self.builder.with_script_params(*self.get_script_params());
366            self.builder.with_fee_params(*self.get_fee_params());
367            self.builder.with_base_asset_id(*self.get_base_asset_id());
368            self.builder
369                .finalize_checked_with_storage(self.block_height, &self.storage)
370        }
371
372        pub fn get_tx_params(&self) -> &TxParameters {
373            self.consensus_params.tx_params()
374        }
375
376        pub fn get_predicate_params(&self) -> &PredicateParameters {
377            self.consensus_params.predicate_params()
378        }
379
380        pub fn get_script_params(&self) -> &ScriptParameters {
381            self.consensus_params.script_params()
382        }
383
384        pub fn get_contract_params(&self) -> &ContractParameters {
385            self.consensus_params.contract_params()
386        }
387
388        pub fn get_fee_params(&self) -> &FeeParameters {
389            self.consensus_params.fee_params()
390        }
391
392        pub fn get_base_asset_id(&self) -> &AssetId {
393            self.consensus_params.base_asset_id()
394        }
395
396        pub fn get_block_gas_limit(&self) -> u64 {
397            self.consensus_params.block_gas_limit()
398        }
399
400        pub fn get_block_transaction_size_limit(&self) -> u64 {
401            self.consensus_params.block_transaction_size_limit()
402        }
403
404        pub fn get_privileged_address(&self) -> &Address {
405            self.consensus_params.privileged_address()
406        }
407
408        pub fn get_chain_id(&self) -> ChainId {
409            self.consensus_params.chain_id()
410        }
411
412        pub fn get_gas_costs(&self) -> &GasCosts {
413            self.consensus_params.gas_costs()
414        }
415
416        pub fn build_get_balance_tx(
417            contract_id: &ContractId,
418            asset_id: &AssetId,
419            tx_offset: usize,
420        ) -> Checked<Script> {
421            let (script, _) = script_with_data_offset!(
422                data_offset,
423                vec![
424                    op::movi(0x11, data_offset),
425                    op::addi(
426                        0x12,
427                        0x11,
428                        Immediate12::try_from(AssetId::LEN)
429                            .expect("`AssetId::LEN` is 32 bytes")
430                    ),
431                    op::bal(0x10, 0x11, 0x12),
432                    op::log(0x10, RegId::ZERO, RegId::ZERO, RegId::ZERO),
433                    op::ret(RegId::ONE),
434                ],
435                tx_offset
436            );
437
438            let script_data: Vec<u8> = [asset_id.as_ref(), contract_id.as_ref()]
439                .into_iter()
440                .flatten()
441                .copied()
442                .collect();
443
444            TestBuilder::new(2322u64)
445                .start_script(script, script_data)
446                .gas_price(0)
447                .script_gas_limit(1_000_000)
448                .contract_input(*contract_id)
449                .fee_input()
450                .contract_output(contract_id)
451                .build()
452        }
453
454        pub fn setup_contract_bytes(
455            &mut self,
456            contract: Vec<u8>,
457            initial_balance: Option<(AssetId, Word)>,
458            initial_state: Option<Vec<StorageSlot>>,
459        ) -> CreatedContract {
460            self.setup_contract_inner(contract, initial_balance, initial_state)
461        }
462
463        pub fn setup_contract(
464            &mut self,
465            contract: Vec<Instruction>,
466            initial_balance: Option<(AssetId, Word)>,
467            initial_state: Option<Vec<StorageSlot>>,
468        ) -> CreatedContract {
469            let contract = contract.into_iter().collect();
470
471            self.setup_contract_inner(contract, initial_balance, initial_state)
472        }
473
474        fn setup_contract_inner(
475            &mut self,
476            contract: Vec<u8>,
477            initial_balance: Option<(AssetId, Word)>,
478            initial_state: Option<Vec<StorageSlot>>,
479        ) -> CreatedContract {
480            let storage_slots = initial_state.unwrap_or_default();
481
482            let salt: Salt = self.rng.gen();
483            let program: Witness = contract.into();
484            let storage_root = Contract::initial_state_root(storage_slots.iter());
485            let contract = Contract::from(program.as_ref());
486            let contract_root = contract.root();
487            let contract_id = contract.id(&salt, &contract_root, &storage_root);
488
489            let tx = TransactionBuilder::create(program, salt, storage_slots)
490                .max_fee_limit(self.max_fee_limit)
491                .maturity(Default::default())
492                .add_fee_input()
493                .add_contract_created()
494                .finalize()
495                .into_checked(self.block_height, &self.consensus_params)
496                .expect("failed to check tx");
497
498            // setup a contract in current test state
499            let state = self
500                .deploy(tx)
501                .expect("Expected vm execution to be successful");
502
503            // set initial contract balance
504            if let Some((asset_id, amount)) = initial_balance {
505                self.storage
506                    .contract_asset_id_balance_insert(&contract_id, &asset_id, amount)
507                    .unwrap();
508            }
509
510            CreatedContract {
511                tx: state.tx().clone(),
512                contract_id,
513                salt,
514            }
515        }
516
517        pub fn setup_blob(&mut self, data: Vec<u8>) {
518            let id = BlobId::compute(data.as_slice());
519
520            let tx = TransactionBuilder::blob(BlobBody {
521                id,
522                witness_index: 0,
523            })
524            .add_witness(data.into())
525            .max_fee_limit(self.max_fee_limit)
526            .maturity(Default::default())
527            .add_fee_input()
528            .finalize()
529            .into_checked(self.block_height, &self.consensus_params)
530            .expect("failed to check tx");
531
532            let interpreter_params =
533                InterpreterParams::new(self.gas_price, &self.consensus_params);
534            let mut transactor = Transactor::<_, _, _>::new(
535                MemoryInstance::new(),
536                self.storage.clone(),
537                interpreter_params,
538            );
539
540            self.execute_tx_inner(&mut transactor, tx)
541                .expect("Expected vm execution to be successful");
542        }
543
544        fn execute_tx_inner<M, Tx, Ecal>(
545            &mut self,
546            transactor: &mut Transactor<M, MemoryStorage, Tx, Ecal>,
547            checked: Checked<Tx>,
548        ) -> anyhow::Result<StateTransition<Tx>>
549        where
550            M: Memory,
551            Tx: ExecutableTransaction,
552            <Tx as IntoChecked>::Metadata: CheckedMetadata,
553            Ecal: crate::interpreter::EcalHandler,
554        {
555            self.storage.set_block_height(self.block_height);
556
557            transactor.transact(checked);
558
559            let storage = transactor.as_mut().clone();
560
561            if let Some(e) = transactor.error() {
562                return Err(anyhow!("{:?}", e));
563            }
564            let is_reverted = transactor.is_reverted();
565
566            let state = transactor.to_owned_state_transition().unwrap();
567
568            let interpreter = transactor.interpreter();
569
570            // verify serialized tx == referenced tx
571            let transaction: Transaction = interpreter.transaction().clone().into();
572            let tx_offset = self.get_tx_params().tx_offset();
573            let mut tx_mem = interpreter
574                .memory()
575                .read(tx_offset, transaction.size())
576                .unwrap();
577            let mut deser_tx = Transaction::decode(&mut tx_mem).unwrap();
578
579            // Patch the tx with correct receipts root
580            if let Transaction::Script(ref mut s) = deser_tx {
581                *s.receipts_root_mut() = interpreter.compute_receipts_root();
582            }
583
584            assert_eq!(deser_tx, transaction);
585            if is_reverted {
586                return Ok(state);
587            }
588
589            // save storage between client instances
590            self.storage = storage;
591
592            Ok(state)
593        }
594
595        pub fn deploy(
596            &mut self,
597            checked: Checked<Create>,
598        ) -> anyhow::Result<StateTransition<Create>> {
599            let interpreter_params =
600                InterpreterParams::new(self.gas_price, &self.consensus_params);
601            let mut transactor = Transactor::<_, _, _>::new(
602                MemoryInstance::new(),
603                self.storage.clone(),
604                interpreter_params,
605            );
606
607            self.execute_tx_inner(&mut transactor, checked)
608        }
609
610        pub fn execute_tx(
611            &mut self,
612            checked: Checked<Script>,
613        ) -> anyhow::Result<StateTransition<Script>> {
614            let interpreter_params =
615                InterpreterParams::new(self.gas_price, &self.consensus_params);
616            let mut transactor = Transactor::<_, _, _>::new(
617                MemoryInstance::new(),
618                self.storage.clone(),
619                interpreter_params,
620            );
621
622            self.execute_tx_inner(&mut transactor, checked)
623        }
624
625        pub fn execute_tx_with_backtrace(
626            &mut self,
627            checked: Checked<Script>,
628            gas_price: u64,
629        ) -> anyhow::Result<(StateTransition<Script>, Option<Backtrace>)> {
630            let interpreter_params =
631                InterpreterParams::new(gas_price, &self.consensus_params);
632            let mut transactor = Transactor::<_, _, _>::new(
633                MemoryInstance::new(),
634                self.storage.clone(),
635                interpreter_params,
636            );
637
638            let state = self.execute_tx_inner(&mut transactor, checked)?;
639            let backtrace = transactor.backtrace();
640
641            Ok((state, backtrace))
642        }
643
644        /// Build test tx and execute it
645        pub fn execute(&mut self) -> StateTransition<Script> {
646            let tx = self.build();
647
648            self.execute_tx(tx)
649                .expect("expected successful vm execution")
650        }
651
652        pub fn get_storage(&self) -> &MemoryStorage {
653            &self.storage
654        }
655
656        pub fn execute_get_outputs(&mut self) -> Vec<Output> {
657            self.execute().tx().outputs().to_vec()
658        }
659
660        pub fn execute_get_change(&mut self, find_asset_id: AssetId) -> Word {
661            let outputs = self.execute_get_outputs();
662            find_change(outputs, find_asset_id)
663        }
664
665        pub fn get_contract_balance(
666            &mut self,
667            contract_id: &ContractId,
668            asset_id: &AssetId,
669        ) -> Word {
670            let tx = TestBuilder::build_get_balance_tx(
671                contract_id,
672                asset_id,
673                self.consensus_params.tx_params().tx_offset(),
674            );
675            let state = self
676                .execute_tx(tx)
677                .expect("expected successful vm execution in this context");
678            let receipts = state.receipts();
679            receipts[0].ra().expect("Balance expected")
680        }
681    }
682
683    pub fn check_expected_reason_for_instructions(
684        instructions: Vec<Instruction>,
685        expected_reason: PanicReason,
686    ) {
687        let client = MemoryClient::default();
688
689        check_expected_reason_for_instructions_with_client(
690            client,
691            instructions,
692            expected_reason,
693        );
694    }
695
696    pub fn check_expected_reason_for_instructions_with_client<M>(
697        mut client: MemoryClient<M>,
698        instructions: Vec<Instruction>,
699        expected_reason: PanicReason,
700    ) where
701        M: Memory,
702    {
703        let tx_params = TxParameters::default().with_max_gas_per_tx(Word::MAX / 2);
704        // The gas should be huge enough to cover the execution but still much less than
705        // `MAX_GAS_PER_TX`.
706        let gas_limit = tx_params.max_gas_per_tx() / 2;
707        let maturity = Default::default();
708        let height = Default::default();
709        let zero_fee_limit = 0;
710
711        // setup contract with state tests
712        let contract: Witness = instructions.into_iter().collect::<Vec<u8>>().into();
713        let salt = Default::default();
714        let code_root = Contract::root_from_code(contract.as_ref());
715        let storage_slots = vec![];
716        let state_root = Contract::initial_state_root(storage_slots.iter());
717        let contract_id =
718            Contract::from(contract.as_ref()).id(&salt, &code_root, &state_root);
719
720        let contract_deployer = TransactionBuilder::create(contract, salt, storage_slots)
721            .max_fee_limit(zero_fee_limit)
722            .with_tx_params(tx_params)
723            .add_fee_input()
724            .add_contract_created()
725            .finalize_checked(height);
726
727        client
728            .deploy(contract_deployer)
729            .expect("valid contract deployment");
730
731        // call deployed contract
732        let script = [
733            // load call data to 0x10
734            op::gtf(0x10, 0x0, Immediate12::from(GTFArgs::ScriptData)),
735            // call the transfer contract
736            op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS),
737            op::ret(RegId::ONE),
738        ]
739        .into_iter()
740        .collect();
741        let script_data: Vec<u8> = [Call::new(contract_id, 0, 0).to_bytes().as_slice()]
742            .into_iter()
743            .flatten()
744            .copied()
745            .collect();
746
747        let tx_deploy_loader = TransactionBuilder::script(script, script_data)
748            .max_fee_limit(zero_fee_limit)
749            .script_gas_limit(gas_limit)
750            .maturity(maturity)
751            .with_tx_params(tx_params)
752            .add_input(Input::contract(
753                Default::default(),
754                Default::default(),
755                Default::default(),
756                Default::default(),
757                contract_id,
758            ))
759            .add_fee_input()
760            .add_output(Output::contract(0, Default::default(), Default::default()))
761            .finalize_checked(height);
762
763        check_reason_for_transaction(client, tx_deploy_loader, expected_reason);
764    }
765
766    pub fn check_reason_for_transaction<M>(
767        mut client: MemoryClient<M>,
768        checked_tx: Checked<Script>,
769        expected_reason: PanicReason,
770    ) where
771        M: Memory,
772    {
773        let receipts = client.transact(checked_tx);
774
775        let panic_found = receipts.iter().any(|receipt| {
776            if let Receipt::Panic { id: _, reason, .. } = receipt {
777                assert_eq!(
778                    &expected_reason,
779                    reason.reason(),
780                    "Expected {}, found {}",
781                    expected_reason,
782                    reason.reason()
783                );
784                true
785            } else {
786                false
787            }
788        });
789
790        if !panic_found {
791            panic!("Script should have panicked");
792        }
793    }
794
795    pub fn find_change(outputs: Vec<Output>, find_asset_id: AssetId) -> Word {
796        let change = outputs.into_iter().find_map(|output| {
797            if let Output::Change {
798                amount, asset_id, ..
799            } = output
800            {
801                if asset_id == find_asset_id {
802                    Some(amount)
803                } else {
804                    None
805                }
806            } else {
807                None
808            }
809        });
810        change.unwrap_or_else(|| {
811            panic!("no change matching asset ID {:x} was found", &find_asset_id)
812        })
813    }
814}
815
816#[allow(missing_docs)]
817#[cfg(all(
818    feature = "profile-gas",
819    feature = "std",
820    any(test, feature = "test-helpers")
821))]
822/// Gas testing utilities
823pub mod gas_profiling {
824    use crate::prelude::*;
825
826    use std::sync::{
827        Arc,
828        Mutex,
829    };
830
831    #[derive(Clone)]
832    pub struct GasProfiler {
833        data: Arc<Mutex<Option<ProfilingData>>>,
834    }
835
836    impl Default for GasProfiler {
837        fn default() -> Self {
838            Self {
839                data: Arc::new(Mutex::new(None)),
840            }
841        }
842    }
843
844    impl ProfileReceiver for GasProfiler {
845        fn on_transaction(
846            &mut self,
847            _state: Result<&ProgramState, InterpreterError<String>>,
848            data: &ProfilingData,
849        ) {
850            let mut guard = self.data.lock().unwrap();
851            *guard = Some(data.clone());
852        }
853    }
854
855    impl GasProfiler {
856        pub fn data(&self) -> Option<ProfilingData> {
857            self.data.lock().ok().and_then(|g| g.as_ref().cloned())
858        }
859
860        pub fn total_gas(&self) -> Word {
861            self.data()
862                .map(|d| {
863                    d.gas()
864                        .iter()
865                        .map(|(_, gas)| gas)
866                        .copied()
867                        .reduce(Word::saturating_add)
868                        .unwrap_or_default()
869                })
870                .unwrap_or_default()
871        }
872    }
873}