radix_engine/transaction/
transaction_receipt.rs

1use super::*;
2use crate::blueprints::consensus_manager::EpochChangeEvent;
3use crate::errors::*;
4use crate::internal_prelude::*;
5use crate::kernel::kernel_callback_api::ExecutionReceipt;
6use crate::system::system_db_reader::SystemDatabaseReader;
7use crate::system::system_modules::costing::*;
8use crate::system::system_modules::execution_trace::*;
9use crate::system::system_substate_schemas::*;
10use crate::transaction::SystemStructure;
11use colored::*;
12use radix_engine_interface::blueprints::transaction_processor::InstructionOutput;
13use radix_transactions::prelude::*;
14use sbor::representations::*;
15
16/// This type is not intended to be encoded or have a consistent scrypto encoding.
17/// Some of the parts of it are encoded in the node, but not the receipt itself.
18#[derive(Clone, PartialEq, Eq)]
19pub struct TransactionReceipt {
20    /// Costing parameters
21    pub costing_parameters: CostingParameters,
22    /// Transaction costing parameters
23    pub transaction_costing_parameters: TransactionCostingParametersReceiptV2,
24    /// Transaction fee summary
25    pub fee_summary: TransactionFeeSummary,
26    /// Transaction fee detail
27    /// Available if `ExecutionConfig::enable_cost_breakdown` is enabled
28    pub fee_details: Option<TransactionFeeDetails>,
29    /// Transaction result
30    pub result: TransactionResult,
31    /// Hardware resources usage report
32    /// Available if `resources_usage` feature flag is enabled
33    pub resources_usage: Option<ResourcesUsage>,
34    /// This field contains debug information about the transaction which is extracted during the
35    /// transaction execution.
36    pub debug_information: Option<TransactionDebugInformation>,
37}
38
39// Type for backwards compatibility to avoid integrator compile errors
40// when they update.
41pub type TransactionReceiptV1 = TransactionReceipt;
42
43#[cfg(all(feature = "std", feature = "flamegraph"))]
44impl TransactionReceipt {
45    pub fn generate_execution_breakdown_flamegraph_svg_bytes(
46        &self,
47        title: impl AsRef<str>,
48        network_definition: &NetworkDefinition,
49    ) -> Result<Vec<u8>, FlamegraphError> {
50        let title = title.as_ref();
51
52        // The options to use when constructing the flamechart.
53        let mut opts = inferno::flamegraph::Options::default();
54        "Execution Cost Units".clone_into(&mut opts.count_name);
55        opts.title = title.to_owned();
56
57        // Transforming the detailed execution cost breakdown into a string understood by the flamegraph
58        // library.
59        let Some(TransactionDebugInformation {
60            ref detailed_execution_cost_breakdown,
61            ..
62        }) = self.debug_information
63        else {
64            return Err(FlamegraphError::DetailedCostBreakdownNotAvailable);
65        };
66
67        let flamegraph_string = Self::transform_detailed_execution_breakdown_into_flamegraph_string(
68            detailed_execution_cost_breakdown,
69            network_definition,
70        );
71
72        // Writing the flamegraph string to a temporary file since its required by the flamegraph lib to
73        // have a path.
74        let result = {
75            let tempfile = tempfile::NamedTempFile::new().map_err(FlamegraphError::IOError)?;
76            std::fs::write(&tempfile, flamegraph_string).map_err(FlamegraphError::IOError)?;
77
78            let mut result = std::io::Cursor::new(Vec::new());
79            inferno::flamegraph::from_files(&mut opts, &[tempfile.path().to_owned()], &mut result)
80                .map_err(|_| FlamegraphError::CreationError)?;
81
82            result.set_position(0);
83            result.into_inner()
84        };
85
86        Ok(result)
87    }
88
89    fn transform_detailed_execution_breakdown_into_flamegraph_string(
90        detailed_execution_cost_breakdown: &[DetailedExecutionCostBreakdownEntry],
91        network_definition: &NetworkDefinition,
92    ) -> String {
93        // Putting use in here so it doesn't cause unused import compile warning in no-std
94        use crate::system::actor::*;
95
96        let address_bech32m_encoder = AddressBech32Encoder::new(&network_definition);
97
98        let mut lines = Vec::<String>::new();
99        let mut path_stack = vec![];
100        for (
101            index,
102            DetailedExecutionCostBreakdownEntry {
103                item: execution_item,
104                ..
105            },
106        ) in detailed_execution_cost_breakdown.iter().enumerate()
107        {
108            // Constructing the full path
109            match execution_item {
110                ExecutionCostBreakdownItem::Invocation { actor, .. } => {
111                    let actor_string = match actor {
112                        Actor::Root => "root".to_owned(),
113                        Actor::Method(MethodActor {
114                            node_id,
115                            ref ident,
116                            ref object_info,
117                            ..
118                        }) => {
119                            format!(
120                                "Method <{}>::{}::{}",
121                                address_bech32m_encoder
122                                    .encode(node_id.as_bytes())
123                                    .expect("Encoding of an address can't fail"),
124                                object_info.blueprint_info.blueprint_id.blueprint_name,
125                                ident
126                            )
127                        }
128                        Actor::Function(FunctionActor {
129                            ref blueprint_id,
130                            ref ident,
131                            ..
132                        }) => {
133                            format!(
134                                "Function <{}>::{}::{}",
135                                address_bech32m_encoder
136                                    .encode(blueprint_id.package_address.as_bytes())
137                                    .expect("Encoding of an address can't fail"),
138                                blueprint_id.blueprint_name,
139                                ident
140                            )
141                        }
142                        Actor::BlueprintHook(BlueprintHookActor {
143                            hook,
144                            ref blueprint_id,
145                            ..
146                        }) => {
147                            format!(
148                                "Blueprint Hook <{}>::{}::{:?}",
149                                address_bech32m_encoder
150                                    .encode(blueprint_id.package_address.as_bytes())
151                                    .expect("Encoding of an address can't fail"),
152                                blueprint_id.blueprint_name,
153                                hook
154                            )
155                        }
156                    };
157                    path_stack.push(format!("Invocation: {actor_string} ({index})"))
158                }
159                ExecutionCostBreakdownItem::InvocationComplete => {
160                    path_stack.pop();
161                }
162                ExecutionCostBreakdownItem::Execution {
163                    simple_name,
164                    cost_units,
165                    ..
166                } => {
167                    lines.push(format!(
168                        "{}{}({}) {}",
169                        if path_stack.join(";").is_empty() {
170                            "".to_owned()
171                        } else {
172                            format!("{};", path_stack.join(";"))
173                        },
174                        simple_name,
175                        index,
176                        cost_units
177                    ));
178                }
179            }
180        }
181
182        lines.join("\n")
183    }
184}
185
186impl ExecutionReceipt for TransactionReceipt {
187    fn set_resource_usage(&mut self, resources_usage: ResourcesUsage) {
188        self.resources_usage = Some(resources_usage);
189    }
190}
191
192#[derive(Default, Debug, Clone, ScryptoSbor, PartialEq, Eq)]
193pub struct TransactionFeeSummary {
194    /// Total execution cost units consumed.
195    pub total_execution_cost_units_consumed: u32,
196    /// Total finalization cost units consumed.
197    pub total_finalization_cost_units_consumed: u32,
198
199    /// Total execution cost in XRD.
200    pub total_execution_cost_in_xrd: Decimal,
201    /// Total finalization cost in XRD.
202    pub total_finalization_cost_in_xrd: Decimal,
203    /// Total tipping cost in XRD.
204    pub total_tipping_cost_in_xrd: Decimal,
205    /// Total storage cost in XRD.
206    pub total_storage_cost_in_xrd: Decimal,
207    /// Total royalty cost in XRD.
208    pub total_royalty_cost_in_xrd: Decimal,
209}
210
211#[derive(Default, Debug, Clone, ScryptoSbor, PartialEq, Eq)]
212pub struct TransactionFeeDetails {
213    /// Execution cost breakdown
214    pub execution_cost_breakdown: BTreeMap<String, u32>,
215    /// Finalization cost breakdown
216    pub finalization_cost_breakdown: BTreeMap<String, u32>,
217}
218
219/// Captures whether a transaction should be committed, and its other results
220#[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)]
221pub enum TransactionResult {
222    Commit(CommitResult),
223    Reject(RejectResult),
224    Abort(AbortResult),
225}
226
227#[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)]
228pub struct CommitResult {
229    /// Substate updates
230    pub state_updates: StateUpdates,
231    /// Information extracted from the substate updates
232    pub state_update_summary: StateUpdateSummary,
233    /// The source of transaction fee
234    pub fee_source: FeeSource,
235    /// The destination of transaction fee
236    pub fee_destination: FeeDestination,
237    /// Transaction execution outcome
238    pub outcome: TransactionOutcome,
239    /// Events emitted
240    pub application_events: Vec<(EventTypeIdentifier, Vec<u8>)>,
241    /// Logs emitted
242    pub application_logs: Vec<(Level, String)>,
243    /// Additional annotation on substates and events
244    pub system_structure: SystemStructure,
245    /// Transaction execution traces
246    /// Available if `ExecutionTrace` module is enabled
247    pub execution_trace: Option<TransactionExecutionTrace>,
248    /// The actually performed nullifications.
249    /// For example, a failed transaction won't include subintent nullifications.
250    pub performed_nullifications: Vec<Nullification>,
251}
252
253#[derive(Debug, Clone, Default, ScryptoSbor, PartialEq, Eq)]
254pub struct FeeSource {
255    pub paying_vaults: IndexMap<NodeId, Decimal>,
256}
257
258#[derive(Debug, Clone, Default, ScryptoSbor, PartialEq, Eq)]
259pub struct FeeDestination {
260    pub to_proposer: Decimal,
261    pub to_validator_set: Decimal,
262    pub to_burn: Decimal,
263    pub to_royalty_recipients: IndexMap<RoyaltyRecipient, Decimal>,
264}
265
266/// Captures whether a transaction's commit outcome is Success or Failure
267#[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)]
268pub enum TransactionOutcome {
269    Success(Vec<InstructionOutput>),
270    Failure(RuntimeError),
271}
272
273#[derive(Debug, Clone, ScryptoSbor, Default, PartialEq, Eq)]
274pub struct TransactionExecutionTrace {
275    pub execution_traces: Vec<ExecutionTrace>,
276    pub resource_changes: IndexMap<usize, Vec<ResourceChange>>,
277    pub fee_locks: FeeLocks,
278}
279
280#[derive(Debug, Clone, Eq, PartialEq, ScryptoSbor, Default)]
281pub struct FeeLocks {
282    pub lock: Decimal,
283    pub contingent_lock: Decimal,
284}
285
286#[derive(Debug, Copy, Clone, Eq, PartialEq, ScryptoSbor)]
287pub enum Nullification {
288    Intent {
289        expiry_epoch: Epoch,
290        intent_hash: IntentHash,
291    },
292}
293
294impl Nullification {
295    pub fn of_intent(
296        intent_hash_nullification: IntentHashNullification,
297        current_epoch: Epoch,
298        is_success: bool,
299    ) -> Option<Self> {
300        let (intent_hash, expiry_epoch) = match intent_hash_nullification {
301            IntentHashNullification::TransactionIntent {
302                intent_hash,
303                expiry_epoch,
304            } => (intent_hash.into(), expiry_epoch),
305            IntentHashNullification::SimulatedTransactionIntent { simulated } => {
306                let intent_hash = simulated.transaction_intent_hash();
307                let expiry_epoch = simulated.expiry_epoch(current_epoch);
308                (intent_hash.into(), expiry_epoch)
309            }
310            IntentHashNullification::Subintent {
311                intent_hash: subintent_hash,
312                expiry_epoch,
313            } => {
314                // Don't write subintent nullification on failure.
315                // Subintents can't pay fees, so this isn't abusable.
316                if !is_success {
317                    return None;
318                }
319                (subintent_hash.into(), expiry_epoch)
320            }
321            IntentHashNullification::SimulatedSubintent { simulated } => {
322                if !is_success {
323                    return None;
324                }
325                let subintent_hash = simulated.subintent_hash();
326                let expiry_epoch = simulated.expiry_epoch(current_epoch);
327                (subintent_hash.into(), expiry_epoch)
328            }
329        };
330        Some(Nullification::Intent {
331            expiry_epoch,
332            intent_hash,
333        })
334    }
335
336    pub fn transaction_tracker_keys(self) -> (Epoch, Hash) {
337        match self {
338            Nullification::Intent {
339                expiry_epoch,
340                intent_hash,
341            } => (expiry_epoch, intent_hash.into_hash()),
342        }
343    }
344}
345
346#[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)]
347pub struct RejectResult {
348    pub reason: RejectionReason,
349}
350
351#[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)]
352pub struct AbortResult {
353    pub reason: AbortReason,
354}
355
356#[derive(Debug, Clone, Display, PartialEq, Eq, Sbor)]
357pub enum AbortReason {
358    ConfiguredAbortTriggeredOnFeeLoanRepayment,
359}
360
361#[derive(Debug, Clone, Default, ScryptoSbor, PartialEq, Eq)]
362pub struct ResourcesUsage {
363    pub heap_allocations_sum: usize,
364    pub heap_peak_memory: usize,
365    pub cpu_cycles: u64,
366}
367
368/// A structure of debug information about the transaction execution.
369///
370/// This is intentionally not SBOR codable since we never want this data to be persisted or
371/// transmitted over the wire.
372#[derive(Clone, PartialEq, Eq)]
373pub struct TransactionDebugInformation {
374    /* Costing Breakdown */
375    /// A detailed trace of where execution cost units were consumed.
376    pub detailed_execution_cost_breakdown: Vec<DetailedExecutionCostBreakdownEntry>,
377}
378
379impl TransactionExecutionTrace {
380    pub fn worktop_changes(&self) -> IndexMap<usize, Vec<WorktopChange>> {
381        let mut aggregator = index_map_new::<usize, Vec<WorktopChange>>();
382        for trace in &self.execution_traces {
383            trace.worktop_changes(&mut aggregator)
384        }
385        aggregator
386    }
387}
388
389impl TransactionResult {
390    pub fn is_commit_success(&self) -> bool {
391        match self {
392            TransactionResult::Commit(c) => matches!(c.outcome, TransactionOutcome::Success(_)),
393            _ => false,
394        }
395    }
396}
397
398impl CommitResult {
399    pub fn empty_with_outcome(outcome: TransactionOutcome) -> Self {
400        Self {
401            state_updates: Default::default(),
402            state_update_summary: Default::default(),
403            fee_source: Default::default(),
404            fee_destination: Default::default(),
405            outcome,
406            application_events: Default::default(),
407            application_logs: Default::default(),
408            system_structure: Default::default(),
409            execution_trace: Default::default(),
410            performed_nullifications: Default::default(),
411        }
412    }
413
414    pub fn next_epoch(&self) -> Option<EpochChangeEvent> {
415        // Note: Node should use a well-known index id
416        for (ref event_type_id, ref event_data) in self.application_events.iter() {
417            let is_consensus_manager = match &event_type_id.0 {
418                Emitter::Method(node_id, ModuleId::Main)
419                    if node_id.entity_type() == Some(EntityType::GlobalConsensusManager) =>
420                {
421                    true
422                }
423                Emitter::Function(blueprint_id)
424                    if blueprint_id.package_address.eq(&CONSENSUS_MANAGER_PACKAGE) =>
425                {
426                    true
427                }
428                _ => false,
429            };
430
431            if is_consensus_manager {
432                if let Ok(epoch_change_event) = scrypto_decode::<EpochChangeEvent>(&event_data) {
433                    return Some(epoch_change_event);
434                }
435            }
436        }
437        None
438    }
439
440    pub fn new_package_addresses(&self) -> &IndexSet<PackageAddress> {
441        &self.state_update_summary.new_packages
442    }
443
444    pub fn new_component_addresses(&self) -> &IndexSet<ComponentAddress> {
445        &self.state_update_summary.new_components
446    }
447
448    pub fn new_resource_addresses(&self) -> &IndexSet<ResourceAddress> {
449        &self.state_update_summary.new_resources
450    }
451
452    pub fn new_vault_addresses(&self) -> &IndexSet<InternalAddress> {
453        &self.state_update_summary.new_vaults
454    }
455
456    pub fn vault_balance_changes(&self) -> &IndexMap<NodeId, (ResourceAddress, BalanceChange)> {
457        &self.state_update_summary.vault_balance_changes
458    }
459
460    pub fn output<T: ScryptoDecode>(&self, nth: usize) -> T {
461        match &self.outcome {
462            TransactionOutcome::Success(o) => match o.get(nth) {
463                Some(InstructionOutput::CallReturn(value)) => {
464                    scrypto_decode::<T>(value).expect("Output can't be converted")
465                }
466                _ => panic!("No output for [{}]", nth),
467            },
468            TransactionOutcome::Failure(_) => panic!("Transaction failed"),
469        }
470    }
471
472    pub fn state_updates(
473        &self,
474    ) -> BTreeMap<NodeId, BTreeMap<PartitionNumber, BTreeMap<SubstateKey, DatabaseUpdate>>> {
475        let mut updates = BTreeMap::<
476            NodeId,
477            BTreeMap<PartitionNumber, BTreeMap<SubstateKey, DatabaseUpdate>>,
478        >::new();
479        for (node_id, x) in &self.state_updates.by_node {
480            let NodeStateUpdates::Delta { by_partition } = x;
481            for (partition_num, y) in by_partition {
482                match y {
483                    PartitionStateUpdates::Delta { by_substate } => {
484                        for (substate_key, substate_update) in by_substate {
485                            updates
486                                .entry(node_id.clone())
487                                .or_default()
488                                .entry(partition_num.clone())
489                                .or_default()
490                                .insert(substate_key.clone(), substate_update.clone());
491                        }
492                    }
493                    PartitionStateUpdates::Batch(BatchPartitionStateUpdate::Reset {
494                        new_substate_values,
495                    }) => {
496                        for (substate_key, substate_value) in new_substate_values {
497                            updates
498                                .entry(node_id.clone())
499                                .or_default()
500                                .entry(partition_num.clone())
501                                .or_default()
502                                .insert(
503                                    substate_key.clone(),
504                                    DatabaseUpdate::Set(substate_value.clone()),
505                                );
506                        }
507                    }
508                }
509            }
510        }
511        updates
512    }
513
514    /// Note - there is a better display of these on the receipt, which uses the schemas
515    /// to display clear details
516    pub fn state_updates_string(&self) -> String {
517        let mut buffer = String::new();
518        for (node_id, x) in &self.state_updates() {
519            buffer.push_str(&format!("\n{:?}, {:?}\n", node_id, node_id.entity_type()));
520            for (partition_num, y) in x {
521                buffer.push_str(&format!("    {:?}\n", partition_num));
522                for (substate_key, substate_update) in y {
523                    buffer.push_str(&format!(
524                        "        {}\n",
525                        match substate_key {
526                            SubstateKey::Field(x) => format!("Field: {}", x),
527                            SubstateKey::Map(x) =>
528                                format!("Map: {:?}", scrypto_decode::<ScryptoValue>(&x).unwrap()),
529                            SubstateKey::Sorted(x) => format!(
530                                "Sorted: {:?}, {:?}",
531                                x.0,
532                                scrypto_decode::<ScryptoValue>(&x.1).unwrap()
533                            ),
534                        },
535                    ));
536                    buffer.push_str(&format!(
537                        "        {}\n",
538                        match substate_update {
539                            DatabaseUpdate::Set(x) =>
540                                format!("Set: {:?}", scrypto_decode::<ScryptoValue>(&x).unwrap()),
541                            DatabaseUpdate::Delete => format!("Delete"),
542                        }
543                    ));
544                }
545            }
546        }
547        buffer
548    }
549}
550
551impl TransactionOutcome {
552    pub fn is_success(&self) -> bool {
553        matches!(self, Self::Success(_))
554    }
555
556    pub fn expect_success(&self) -> &Vec<InstructionOutput> {
557        match self {
558            TransactionOutcome::Success(results) => results,
559            TransactionOutcome::Failure(error) => panic!("Outcome was a failure: {error:?}"),
560        }
561    }
562
563    pub fn expect_failure(&self) -> &RuntimeError {
564        match self {
565            TransactionOutcome::Success(_) => panic!("Outcome was an unexpected success"),
566            TransactionOutcome::Failure(error) => error,
567        }
568    }
569
570    pub fn success_or_else<E, F: Fn(&RuntimeError) -> E>(
571        &self,
572        f: F,
573    ) -> Result<&Vec<InstructionOutput>, E> {
574        match self {
575            TransactionOutcome::Success(results) => Ok(results),
576            TransactionOutcome::Failure(error) => Err(f(error)),
577        }
578    }
579}
580
581impl TransactionReceipt {
582    /// An empty receipt for merging changes into.
583    pub fn empty_with_commit(commit_result: CommitResult) -> Self {
584        Self {
585            costing_parameters: CostingParameters::babylon_genesis(),
586            transaction_costing_parameters: Default::default(),
587            fee_summary: Default::default(),
588            fee_details: Default::default(),
589            result: TransactionResult::Commit(commit_result),
590            resources_usage: Default::default(),
591            debug_information: Default::default(),
592        }
593    }
594
595    pub fn empty_commit_success() -> Self {
596        Self::empty_with_commit(CommitResult::empty_with_outcome(
597            TransactionOutcome::Success(vec![]),
598        ))
599    }
600
601    pub fn is_commit_success(&self) -> bool {
602        matches!(
603            self.result,
604            TransactionResult::Commit(CommitResult {
605                outcome: TransactionOutcome::Success(_),
606                ..
607            })
608        )
609    }
610
611    pub fn is_commit_failure(&self) -> bool {
612        matches!(
613            self.result,
614            TransactionResult::Commit(CommitResult {
615                outcome: TransactionOutcome::Failure(_),
616                ..
617            })
618        )
619    }
620
621    pub fn is_rejection(&self) -> bool {
622        matches!(self.result, TransactionResult::Reject(_))
623    }
624
625    pub fn expect_commit_ignore_outcome(&self) -> &CommitResult {
626        match &self.result {
627            TransactionResult::Commit(c) => c,
628            TransactionResult::Reject(e) => panic!("Transaction was rejected: {:?}", e),
629            TransactionResult::Abort(_) => panic!("Transaction was aborted"),
630        }
631    }
632
633    pub fn into_commit_ignore_outcome(self) -> CommitResult {
634        match self.result {
635            TransactionResult::Commit(c) => c,
636            TransactionResult::Reject(e) => panic!("Transaction was rejected: {:?}", e),
637            TransactionResult::Abort(_) => panic!("Transaction was aborted"),
638        }
639    }
640
641    pub fn expect_commit(&self, success: bool) -> &CommitResult {
642        let c = self.expect_commit_ignore_outcome();
643        if c.outcome.is_success() != success {
644            panic!(
645                "Expected {} but was {}: {:?}",
646                if success { "success" } else { "failure" },
647                if c.outcome.is_success() {
648                    "success"
649                } else {
650                    "failure"
651                },
652                c.outcome
653            )
654        }
655        c
656    }
657
658    pub fn expect_commit_success(&self) -> &CommitResult {
659        self.expect_commit(true)
660    }
661
662    pub fn expect_commit_failure(&self) -> &CommitResult {
663        self.expect_commit(false)
664    }
665
666    pub fn expect_commit_failure_containing_error(&self, error_needle: &str) {
667        let error_message = self
668            .expect_commit_failure()
669            .outcome
670            .expect_failure()
671            .to_string(NO_NETWORK);
672        assert!(
673            error_message.contains(error_needle),
674            "{error_needle:?} was not contained in RuntimeError"
675        );
676    }
677
678    pub fn expect_rejection(&self) -> &RejectionReason {
679        match &self.result {
680            TransactionResult::Commit(..) => panic!("Expected rejection but was commit"),
681            TransactionResult::Reject(ref r) => &r.reason,
682            TransactionResult::Abort(..) => panic!("Expected rejection but was abort"),
683        }
684    }
685
686    pub fn expect_rejection_containing_error(&self, error_needle: &str) {
687        let error_message = self.expect_rejection().to_string(NO_NETWORK);
688        assert!(
689            error_message.contains(error_needle),
690            "{error_needle:?} was not contained in RejectionReason"
691        );
692    }
693
694    pub fn expect_abortion(&self) -> &AbortReason {
695        match &self.result {
696            TransactionResult::Commit(..) => panic!("Expected abortion but was commit"),
697            TransactionResult::Reject(..) => panic!("Expected abortion but was reject"),
698            TransactionResult::Abort(ref r) => &r.reason,
699        }
700    }
701
702    pub fn expect_not_success(&self) {
703        match &self.result {
704            TransactionResult::Commit(c) => {
705                if c.outcome.is_success() {
706                    panic!("Transaction succeeded unexpectedly")
707                }
708            }
709            TransactionResult::Reject(..) => {}
710            TransactionResult::Abort(..) => {}
711        }
712    }
713
714    pub fn expect_specific_rejection<F>(&self, f: F)
715    where
716        F: Fn(&RejectionReason) -> bool,
717    {
718        match &self.result {
719            TransactionResult::Commit(..) => panic!("Expected rejection but was committed"),
720            TransactionResult::Reject(result) => {
721                if !f(&result.reason) {
722                    panic!(
723                        "Expected specific rejection but was different error:\n{:?}",
724                        self
725                    );
726                }
727            }
728            TransactionResult::Abort(..) => panic!("Expected rejection but was abort"),
729        }
730    }
731
732    pub fn expect_failure(&self) -> &RuntimeError {
733        match &self.result {
734            TransactionResult::Commit(c) => match &c.outcome {
735                TransactionOutcome::Success(_) => panic!("Expected failure but was success"),
736                TransactionOutcome::Failure(error) => error,
737            },
738            TransactionResult::Reject(_) => panic!("Transaction was rejected"),
739            TransactionResult::Abort(..) => panic!("Transaction was aborted"),
740        }
741    }
742
743    pub fn expect_specific_failure<F>(&self, f: F)
744    where
745        F: Fn(&RuntimeError) -> bool,
746    {
747        if !f(self.expect_failure()) {
748            panic!(
749                "Expected specific failure but was different error:\n{:?}",
750                self
751            );
752        }
753    }
754
755    pub fn expect_auth_failure(&self) {
756        self.expect_specific_failure(|e| {
757            matches!(
758                e,
759                RuntimeError::SystemModuleError(SystemModuleError::AuthError(..))
760            )
761        })
762    }
763
764    pub fn expect_auth_assertion_failure(&self) {
765        self.expect_specific_failure(|e| {
766            matches!(
767                e,
768                RuntimeError::SystemError(SystemError::AssertAccessRuleFailed)
769            )
770        })
771    }
772
773    pub fn effective_execution_cost_unit_price(&self) -> Decimal {
774        // Below unwraps are safe, no chance to overflow considering current costing parameters
775        self.costing_parameters
776            .execution_cost_unit_price
777            .checked_mul(
778                Decimal::ONE
779                    .checked_add(self.transaction_costing_parameters.tip_proportion)
780                    .unwrap(),
781            )
782            .unwrap()
783    }
784
785    pub fn effective_finalization_cost_unit_price(&self) -> Decimal {
786        let one_percent = Decimal::ONE_HUNDREDTH;
787
788        // Below unwraps are safe, no chance to overflow considering current costing parameters
789        self.costing_parameters
790            .finalization_cost_unit_price
791            .checked_mul(
792                Decimal::ONE
793                    .checked_add(
794                        one_percent
795                            .checked_mul(self.transaction_costing_parameters.tip_proportion)
796                            .unwrap(),
797                    )
798                    .unwrap(),
799            )
800            .unwrap()
801    }
802}
803
804macro_rules! prefix {
805    ($i:expr, $list:expr) => {
806        if $i == $list.len() - 1 {
807            "└─"
808        } else {
809            "├─"
810        }
811    };
812}
813
814impl fmt::Debug for TransactionReceipt {
815    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
816        write!(
817            f,
818            "{}",
819            self.display(TransactionReceiptDisplayContext::default())
820        )
821    }
822}
823
824pub struct TransactionReceiptDisplayContext<'a> {
825    pub encoder: Option<&'a AddressBech32Encoder>,
826    pub system_database_reader: Option<SystemDatabaseReader<'a, dyn SubstateDatabase + 'a>>,
827    pub display_state_updates: bool,
828    pub use_ansi_colors: bool,
829    pub max_substate_length_to_display: usize,
830}
831
832impl<'a> Default for TransactionReceiptDisplayContext<'a> {
833    fn default() -> Self {
834        Self {
835            encoder: None,
836            system_database_reader: None,
837            display_state_updates: true,
838            use_ansi_colors: true,
839            max_substate_length_to_display: 1024,
840        }
841    }
842}
843
844impl<'a> TransactionReceiptDisplayContext<'a> {
845    pub fn display_context(&self) -> ScryptoValueDisplayContext<'a> {
846        ScryptoValueDisplayContext::with_optional_bech32(self.encoder)
847    }
848
849    pub fn address_display_context(&self) -> AddressDisplayContext<'a> {
850        AddressDisplayContext {
851            encoder: self.encoder,
852        }
853    }
854
855    pub fn max_substate_length_to_display(&self) -> usize {
856        self.max_substate_length_to_display
857    }
858
859    pub fn lookup_schema<T: AsRef<NodeId>>(
860        &self,
861        full_type_id: &FullyScopedTypeId<T>,
862    ) -> Option<(LocalTypeId, Rc<VersionedScryptoSchema>)> {
863        self.system_database_reader.as_ref().map(|system_reader| {
864            let schema = system_reader
865                .get_schema(full_type_id.0.as_ref(), &full_type_id.1)
866                .unwrap();
867
868            (full_type_id.2.clone(), schema)
869        })
870    }
871
872    fn format_first_top_level_title_with_detail<F: fmt::Write, D: fmt::Display>(
873        &self,
874        f: &mut F,
875        title: &str,
876        detail: D,
877    ) -> Result<(), fmt::Error> {
878        if self.use_ansi_colors {
879            write!(f, "{} {}", format!("{}:", title).bold().green(), detail)
880        } else {
881            write!(f, "{}: {}", title.to_uppercase(), detail)
882        }
883    }
884
885    fn format_top_level_title_with_detail<F: fmt::Write, D: fmt::Display>(
886        &self,
887        f: &mut F,
888        title: &str,
889        detail: D,
890    ) -> Result<(), fmt::Error> {
891        if self.use_ansi_colors {
892            write!(f, "\n{} {}", format!("{}:", title).bold().green(), detail)
893        } else {
894            write!(f, "\n\n{}: {}", title.to_uppercase(), detail)
895        }
896    }
897
898    fn display_title(&self, title: &str) -> MaybeAnsi {
899        if self.use_ansi_colors {
900            MaybeAnsi::Ansi(title.bold().green())
901        } else {
902            MaybeAnsi::Normal(title.to_string())
903        }
904    }
905
906    fn display_result(&self, result: &TransactionResult) -> MaybeAnsi {
907        let (string, format): (String, fn(String) -> ColoredString) = match result {
908            TransactionResult::Commit(c) => match &c.outcome {
909                TransactionOutcome::Success(_) => ("COMMITTED SUCCESS".to_string(), |x| x.green()),
910                TransactionOutcome::Failure(e) => (
911                    format!("COMMITTED FAILURE: {}", e.display(self.display_context())),
912                    |x| x.red(),
913                ),
914            },
915            TransactionResult::Reject(r) => (
916                format!("REJECTED: {}", r.reason.display(self.display_context())),
917                |x| x.red(),
918            ),
919            TransactionResult::Abort(a) => (format!("ABORTED: {}", a.reason), |x| x.bright_red()),
920        };
921        if self.use_ansi_colors {
922            MaybeAnsi::Ansi(format(string))
923        } else {
924            MaybeAnsi::Normal(string)
925        }
926    }
927
928    fn display_log(&self, level: &Level, message: &str) -> (MaybeAnsi, MaybeAnsi) {
929        let (level, format): (_, fn(&str) -> ColoredString) = match level {
930            Level::Error => ("ERROR", |x| x.red()),
931            Level::Warn => ("WARN", |x| x.yellow()),
932            Level::Info => ("INFO", |x| x.green()),
933            Level::Debug => ("DEBUG", |x| x.cyan()),
934            Level::Trace => ("TRACE", |x| x.normal()),
935        };
936
937        if self.use_ansi_colors {
938            (
939                MaybeAnsi::Ansi(format(level)),
940                MaybeAnsi::Ansi(format(message)),
941            )
942        } else {
943            (
944                MaybeAnsi::Normal(level.to_string()),
945                MaybeAnsi::Normal(message.to_string()),
946            )
947        }
948    }
949}
950
951enum MaybeAnsi {
952    Ansi(ColoredString),
953    Normal(String),
954}
955
956impl fmt::Display for MaybeAnsi {
957    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
958        match self {
959            MaybeAnsi::Ansi(value) => write!(f, "{}", value),
960            MaybeAnsi::Normal(value) => write!(f, "{}", value),
961        }
962    }
963}
964
965impl<'a> From<&'a AddressBech32Encoder> for TransactionReceiptDisplayContext<'a> {
966    fn from(encoder: &'a AddressBech32Encoder) -> Self {
967        Self {
968            encoder: Some(encoder),
969            ..Default::default()
970        }
971    }
972}
973
974impl<'a> From<Option<&'a AddressBech32Encoder>> for TransactionReceiptDisplayContext<'a> {
975    fn from(encoder: Option<&'a AddressBech32Encoder>) -> Self {
976        Self {
977            encoder,
978            ..Default::default()
979        }
980    }
981}
982
983pub struct TransactionReceiptDisplayContextBuilder<'a>(TransactionReceiptDisplayContext<'a>);
984
985impl<'a> TransactionReceiptDisplayContextBuilder<'a> {
986    pub fn new() -> Self {
987        Self(Default::default())
988    }
989
990    pub fn encoder(mut self, encoder: &'a AddressBech32Encoder) -> Self {
991        self.0.encoder = Some(encoder);
992        self
993    }
994
995    pub fn schema_lookup_from_db(mut self, db: &'a dyn SubstateDatabase) -> Self {
996        self.0.system_database_reader = Some(SystemDatabaseReader::new(db));
997        self
998    }
999
1000    pub fn display_state_updates(mut self, setting: bool) -> Self {
1001        self.0.display_state_updates = setting;
1002        self
1003    }
1004
1005    pub fn use_ansi_colors(mut self, setting: bool) -> Self {
1006        self.0.use_ansi_colors = setting;
1007        self
1008    }
1009
1010    pub fn set_max_substate_length_to_display(mut self, setting: usize) -> Self {
1011        self.0.max_substate_length_to_display = setting;
1012        self
1013    }
1014
1015    pub fn build(self) -> TransactionReceiptDisplayContext<'a> {
1016        self.0
1017    }
1018}
1019
1020impl<'a> ContextualDisplay<TransactionReceiptDisplayContext<'a>> for TransactionReceipt {
1021    type Error = fmt::Error;
1022
1023    fn contextual_format<F: fmt::Write>(
1024        &self,
1025        f: &mut F,
1026        context: &TransactionReceiptDisplayContext<'a>,
1027    ) -> Result<(), Self::Error> {
1028        let result = &self.result;
1029        let scrypto_value_display_context = context.display_context();
1030        let address_display_context = context.address_display_context();
1031
1032        context.format_first_top_level_title_with_detail(
1033            f,
1034            "Transaction Status",
1035            context.display_result(result),
1036        )?;
1037
1038        context.format_top_level_title_with_detail(
1039            f,
1040            "Transaction Cost",
1041            format!("{} XRD", self.fee_summary.total_cost()),
1042        )?;
1043        write!(
1044            f,
1045            "\n├─ {} {} XRD, {} execution cost units",
1046            context.display_title("Network execution:"),
1047            self.fee_summary.total_execution_cost_in_xrd,
1048            self.fee_summary.total_execution_cost_units_consumed,
1049        )?;
1050        write!(
1051            f,
1052            "\n├─ {} {} XRD, {} finalization cost units",
1053            context.display_title("Network finalization:"),
1054            self.fee_summary.total_finalization_cost_in_xrd,
1055            self.fee_summary.total_finalization_cost_units_consumed,
1056        )?;
1057        write!(
1058            f,
1059            "\n├─ {} {} XRD",
1060            context.display_title("Tip:"),
1061            self.fee_summary.total_tipping_cost_in_xrd
1062        )?;
1063        write!(
1064            f,
1065            "\n├─ {} {} XRD",
1066            context.display_title("Network Storage:"),
1067            self.fee_summary.total_storage_cost_in_xrd
1068        )?;
1069        write!(
1070            f,
1071            "\n└─ {} {} XRD",
1072            context.display_title("Royalties:"),
1073            self.fee_summary.total_royalty_cost_in_xrd
1074        )?;
1075
1076        if let TransactionResult::Commit(c) = &result {
1077            context.format_top_level_title_with_detail(f, "Logs", c.application_logs.len())?;
1078            for (i, (level, msg)) in c.application_logs.iter().enumerate() {
1079                let (level, msg) = context.display_log(level, msg);
1080                write!(
1081                    f,
1082                    "\n{} [{:5}] {}",
1083                    prefix!(i, c.application_logs),
1084                    level,
1085                    msg
1086                )?;
1087            }
1088
1089            context.format_top_level_title_with_detail(f, "Events", c.application_events.len())?;
1090            for (i, (event_type_identifier, event_data)) in c.application_events.iter().enumerate()
1091            {
1092                display_event(
1093                    f,
1094                    prefix!(i, c.application_events),
1095                    event_type_identifier,
1096                    &c.system_structure,
1097                    event_data,
1098                    context,
1099                )?;
1100            }
1101
1102            if context.display_state_updates {
1103                (&c.state_updates, &c.system_structure).contextual_format(f, context)?;
1104            }
1105
1106            if let TransactionOutcome::Success(outputs) = &c.outcome {
1107                context.format_top_level_title_with_detail(f, "Outputs", outputs.len())?;
1108                for (i, output) in outputs.iter().enumerate() {
1109                    write!(
1110                        f,
1111                        "\n{} {}",
1112                        prefix!(i, outputs),
1113                        match output {
1114                            InstructionOutput::CallReturn(x) => IndexedScryptoValue::from_slice(&x)
1115                                .expect("Impossible case! Instruction output can't be decoded")
1116                                .to_string(ValueDisplayParameters::Schemaless {
1117                                    display_mode: DisplayMode::RustLike(RustLikeOptions::full()),
1118                                    print_mode: PrintMode::MultiLine {
1119                                        indent_size: 2,
1120                                        base_indent: 3,
1121                                        first_line_indent: 0
1122                                    },
1123                                    custom_context: scrypto_value_display_context,
1124                                    depth_limit: SCRYPTO_SBOR_V1_MAX_DEPTH
1125                                }),
1126                            InstructionOutput::None => "None".to_string(),
1127                        }
1128                    )?;
1129                }
1130            }
1131
1132            let balance_changes = c.vault_balance_changes();
1133            context.format_top_level_title_with_detail(
1134                f,
1135                "Balance Changes",
1136                balance_changes.len(),
1137            )?;
1138            for (i, (vault_id, (resource, delta))) in balance_changes.iter().enumerate() {
1139                write!(
1140                    f,
1141                    // NB - we use ResAddr instead of Resource to protect people who read new resources as
1142                    //      `Resource: ` from the receipts (see eg resim.sh)
1143                    "\n{} Vault: {}\n   ResAddr: {}\n   Change: {}",
1144                    prefix!(i, balance_changes),
1145                    vault_id.display(address_display_context),
1146                    resource.display(address_display_context),
1147                    match delta {
1148                        BalanceChange::Fungible(d) => format!("{}", d),
1149                        BalanceChange::NonFungible { added, removed } => {
1150                            format!("+{:?}, -{:?}", added, removed)
1151                        }
1152                    }
1153                )?;
1154            }
1155
1156            context.format_top_level_title_with_detail(
1157                f,
1158                "New Entities",
1159                c.new_package_addresses().len()
1160                    + c.new_component_addresses().len()
1161                    + c.new_resource_addresses().len(),
1162            )?;
1163            for (i, package_address) in c.new_package_addresses().iter().enumerate() {
1164                write!(
1165                    f,
1166                    "\n{} Package: {}",
1167                    prefix!(i, c.new_package_addresses()),
1168                    package_address.display(address_display_context)
1169                )?;
1170            }
1171            for (i, component_address) in c.new_component_addresses().iter().enumerate() {
1172                write!(
1173                    f,
1174                    "\n{} Component: {}",
1175                    prefix!(i, c.new_component_addresses()),
1176                    component_address.display(address_display_context)
1177                )?;
1178            }
1179            for (i, resource_address) in c.new_resource_addresses().iter().enumerate() {
1180                write!(
1181                    f,
1182                    "\n{} Resource: {}",
1183                    prefix!(i, c.new_resource_addresses()),
1184                    resource_address.display(address_display_context)
1185                )?;
1186            }
1187        }
1188
1189        Ok(())
1190    }
1191}
1192
1193impl<'a, 'b> ContextualDisplay<TransactionReceiptDisplayContext<'a>>
1194    for (&'b StateUpdates, &'b SystemStructure)
1195{
1196    type Error = fmt::Error;
1197
1198    fn contextual_format<F: fmt::Write>(
1199        &self,
1200        f: &mut F,
1201        context: &TransactionReceiptDisplayContext<'a>,
1202    ) -> Result<(), Self::Error> {
1203        let state_updates = self.0;
1204        let system_structure = self.1;
1205        context.format_top_level_title_with_detail(
1206            f,
1207            "State Updates",
1208            format!(
1209                "{} {}",
1210                state_updates.by_node.len(),
1211                if state_updates.by_node.len() == 1 {
1212                    "entity"
1213                } else {
1214                    "entities"
1215                },
1216            ),
1217        )?;
1218        for (i, (node_id, node_updates)) in state_updates.by_node.iter().enumerate() {
1219            let by_partition = match node_updates {
1220                NodeStateUpdates::Delta { by_partition } => by_partition,
1221            };
1222            write!(
1223                f,
1224                "\n{} {} across {} partitions",
1225                prefix!(i, state_updates.by_node),
1226                node_id.display(context.address_display_context()),
1227                by_partition.len(),
1228            )?;
1229
1230            for (j, (partition_number, partition_updates)) in by_partition.iter().enumerate() {
1231                // NOTE: This could be improved by mapping partition numbers back to a system-focused name
1232                //       This would require either adding partition descriptions into SystemStructure, or
1233                //       having some inverse entity-type specific descriptors.
1234                match partition_updates {
1235                    PartitionStateUpdates::Delta { by_substate } => {
1236                        write!(
1237                            f,
1238                            "\n  {} Partition({}): {} {}",
1239                            prefix!(j, by_partition),
1240                            partition_number.0,
1241                            by_substate.len(),
1242                            if by_substate.len() == 1 {
1243                                "change"
1244                            } else {
1245                                "changes"
1246                            },
1247                        )?;
1248                        for (k, (substate_key, update)) in by_substate.iter().enumerate() {
1249                            display_substate_change(
1250                                f,
1251                                prefix!(k, by_substate),
1252                                system_structure,
1253                                context,
1254                                node_id,
1255                                partition_number,
1256                                substate_key,
1257                                update.as_ref(),
1258                            )?;
1259                        }
1260                    }
1261                    PartitionStateUpdates::Batch(BatchPartitionStateUpdate::Reset {
1262                        new_substate_values,
1263                    }) => {
1264                        write!(
1265                            f,
1266                            "\n {} Partition({}): RESET ({} new values)",
1267                            prefix!(j, by_partition),
1268                            partition_number.0,
1269                            new_substate_values.len()
1270                        )?;
1271                        for (k, (substate_key, value)) in new_substate_values.iter().enumerate() {
1272                            display_substate_change(
1273                                f,
1274                                prefix!(k, new_substate_values),
1275                                system_structure,
1276                                context,
1277                                node_id,
1278                                partition_number,
1279                                substate_key,
1280                                DatabaseUpdateRef::Set(value),
1281                            )?;
1282                        }
1283                    }
1284                }
1285            }
1286        }
1287        Ok(())
1288    }
1289}
1290
1291fn display_substate_change<'a, F: fmt::Write>(
1292    f: &mut F,
1293    prefix: &str,
1294    system_structure: &SystemStructure,
1295    receipt_context: &TransactionReceiptDisplayContext<'a>,
1296    node_id: &NodeId,
1297    partition_number: &PartitionNumber,
1298    substate_key: &SubstateKey,
1299    change: DatabaseUpdateRef,
1300) -> Result<(), fmt::Error> {
1301    let substate_structure = system_structure
1302        .substate_system_structures
1303        .get(node_id)
1304        .unwrap()
1305        .get(partition_number)
1306        .unwrap()
1307        .get(substate_key)
1308        .unwrap();
1309    match change {
1310        DatabaseUpdateRef::Set(substate_value) => {
1311            write!(f, "\n    {prefix} Set: ")?;
1312            format_receipt_substate_key(f, substate_structure, receipt_context, substate_key)?;
1313            write!(f, "\n       Value: ")?;
1314            format_receipt_substate_value(f, substate_structure, receipt_context, substate_value)?;
1315        }
1316        DatabaseUpdateRef::Delete => {
1317            write!(f, "\n    {prefix} Delete: ")?;
1318            format_receipt_substate_key(f, substate_structure, receipt_context, substate_key)?;
1319        }
1320    }
1321    Ok(())
1322}
1323
1324fn format_receipt_substate_key<'a, F: fmt::Write>(
1325    f: &mut F,
1326    substate_structure: &SubstateSystemStructure,
1327    receipt_context: &TransactionReceiptDisplayContext<'a>,
1328    substate_key: &SubstateKey,
1329) -> Result<(), fmt::Error> {
1330    let print_mode = PrintMode::SingleLine;
1331    match substate_structure {
1332        SubstateSystemStructure::SystemField(structure) => {
1333            write!(f, "{:?}", structure.field_kind)
1334        }
1335        SubstateSystemStructure::SystemSchema => {
1336            let key_contents = substate_key.for_map().unwrap();
1337            let hash: SchemaHash = scrypto_decode(&*key_contents).unwrap();
1338            write!(f, "SchemaHash({})", hash.0)
1339        }
1340        SubstateSystemStructure::KeyValueStoreEntry(structure) => {
1341            let value = scrypto_decode(substate_key.for_map().unwrap()).unwrap();
1342            format_scrypto_value_with_full_type_id(
1343                f,
1344                print_mode,
1345                value,
1346                receipt_context,
1347                &structure.key_full_type_id,
1348            )
1349        }
1350        SubstateSystemStructure::ObjectField(_) => {
1351            let key_contents = substate_key.for_field().unwrap();
1352            write!(f, "Field({})", key_contents)
1353        }
1354        SubstateSystemStructure::ObjectKeyValuePartitionEntry(structure) => {
1355            let value = scrypto_decode(substate_key.for_map().unwrap()).unwrap();
1356            let full_type_id = extract_object_type_id(&structure.key_schema);
1357            format_scrypto_value_with_full_type_id(
1358                f,
1359                print_mode,
1360                value,
1361                receipt_context,
1362                &full_type_id,
1363            )
1364        }
1365        SubstateSystemStructure::ObjectIndexPartitionEntry(structure) => {
1366            let value = scrypto_decode(substate_key.for_map().unwrap()).unwrap();
1367            let full_type_id = extract_object_type_id(&structure.key_schema);
1368            format_scrypto_value_with_full_type_id(
1369                f,
1370                print_mode,
1371                value,
1372                receipt_context,
1373                &full_type_id,
1374            )
1375        }
1376        SubstateSystemStructure::ObjectSortedIndexPartitionEntry(structure) => {
1377            let (sort_bytes, key_contents) = substate_key.for_sorted().unwrap();
1378            let value = scrypto_decode(key_contents).unwrap();
1379            let full_type_id = extract_object_type_id(&structure.key_schema);
1380            write!(f, "SortKey({}, ", u16::from_be_bytes(sort_bytes.clone()))?;
1381            format_scrypto_value_with_full_type_id(
1382                f,
1383                print_mode,
1384                value,
1385                receipt_context,
1386                &full_type_id,
1387            )?;
1388            write!(f, ")")
1389        }
1390    }
1391}
1392
1393pub fn format_receipt_substate_value<'a, F: fmt::Write>(
1394    f: &mut F,
1395    substate_structure: &SubstateSystemStructure,
1396    receipt_context: &TransactionReceiptDisplayContext<'a>,
1397    substate_value: &[u8],
1398) -> Result<(), fmt::Error> {
1399    let print_mode = PrintMode::MultiLine {
1400        indent_size: 2,
1401        base_indent: 7,
1402        first_line_indent: 0,
1403    };
1404    if substate_value.len() > receipt_context.max_substate_length_to_display() {
1405        write!(
1406            f,
1407            "(Hidden as longer than {} bytes. Hash: {})",
1408            receipt_context.max_substate_length_to_display(),
1409            hash(substate_value)
1410        )
1411    } else {
1412        let (payload, full_type_id) = match substate_structure {
1413            SubstateSystemStructure::SystemField(structure) => {
1414                let single_type_schema = resolve_system_field_schema(structure.field_kind);
1415                let raw_value = scrypto_decode(substate_value).unwrap();
1416                return format_scrypto_value_with_schema(
1417                    f,
1418                    print_mode,
1419                    raw_value,
1420                    receipt_context,
1421                    &single_type_schema.schema,
1422                    single_type_schema.type_id,
1423                );
1424            }
1425            SubstateSystemStructure::SystemSchema => {
1426                let single_type_schema = resolve_system_schema_schema();
1427                let raw_value = scrypto_decode(substate_value).unwrap();
1428                return format_scrypto_value_with_schema(
1429                    f,
1430                    print_mode,
1431                    raw_value,
1432                    receipt_context,
1433                    &single_type_schema.schema,
1434                    single_type_schema.type_id,
1435                );
1436            }
1437            SubstateSystemStructure::KeyValueStoreEntry(structure) => {
1438                let payload =
1439                    scrypto_decode::<KeyValueEntrySubstate<ScryptoRawValue>>(substate_value)
1440                        .unwrap();
1441                (payload.into_value(), structure.value_full_type_id.clone())
1442            }
1443            SubstateSystemStructure::ObjectField(structure) => {
1444                let payload =
1445                    scrypto_decode::<FieldSubstate<ScryptoRawValue>>(substate_value).unwrap();
1446                write_lock_status(f, payload.lock_status())?;
1447                (
1448                    Some(payload.into_payload()),
1449                    extract_object_type_id(&structure.value_schema),
1450                )
1451            }
1452            SubstateSystemStructure::ObjectKeyValuePartitionEntry(structure) => {
1453                let payload =
1454                    scrypto_decode::<KeyValueEntrySubstate<ScryptoRawValue>>(substate_value)
1455                        .unwrap();
1456                write_lock_status(f, payload.lock_status())?;
1457                (
1458                    payload.into_value(),
1459                    extract_object_type_id(&structure.value_schema),
1460                )
1461            }
1462            SubstateSystemStructure::ObjectIndexPartitionEntry(structure) => {
1463                let payload =
1464                    scrypto_decode::<IndexEntrySubstate<ScryptoRawValue>>(substate_value).unwrap();
1465                (
1466                    Some(payload.into_value()),
1467                    extract_object_type_id(&structure.value_schema),
1468                )
1469            }
1470            SubstateSystemStructure::ObjectSortedIndexPartitionEntry(structure) => {
1471                let payload =
1472                    scrypto_decode::<SortedIndexEntrySubstate<ScryptoRawValue>>(substate_value)
1473                        .unwrap();
1474                (
1475                    Some(payload.into_value()),
1476                    extract_object_type_id(&structure.value_schema),
1477                )
1478            }
1479        };
1480        match payload {
1481            Some(payload) => format_scrypto_value_with_full_type_id(
1482                f,
1483                print_mode,
1484                payload,
1485                receipt_context,
1486                &full_type_id,
1487            ),
1488            None => write!(f, "EMPTY"),
1489        }
1490    }
1491}
1492
1493fn write_lock_status<F: fmt::Write>(f: &mut F, lock_status: LockStatus) -> Result<(), fmt::Error> {
1494    match lock_status {
1495        LockStatus::Unlocked => write!(f, "UNLOCKED "),
1496        LockStatus::Locked => write!(f, "LOCKED "),
1497    }
1498}
1499
1500fn extract_object_type_id(structure: &ObjectSubstateTypeReference) -> FullyScopedTypeId<NodeId> {
1501    match structure {
1502        ObjectSubstateTypeReference::Package(r) => r.full_type_id.clone().into_general(),
1503        ObjectSubstateTypeReference::ObjectInstance(r) => r.resolved_full_type_id.clone(),
1504    }
1505}
1506
1507fn display_event<'a, F: fmt::Write>(
1508    f: &mut F,
1509    prefix: &str,
1510    event_type_identifier: &EventTypeIdentifier,
1511    system_structure: &SystemStructure,
1512    event_data: &[u8],
1513    receipt_context: &TransactionReceiptDisplayContext<'a>,
1514) -> Result<(), fmt::Error> {
1515    let event_system_structure = system_structure
1516        .event_system_structures
1517        .get(event_type_identifier)
1518        .expect("Expected event to appear in the system structure");
1519
1520    let full_type_id = event_system_structure.package_type_reference.full_type_id;
1521    let schema_lookup = receipt_context.lookup_schema(&full_type_id);
1522    let emitter = &event_type_identifier.0;
1523    let print_mode = PrintMode::MultiLine {
1524        indent_size: 2,
1525        base_indent: 3,
1526        first_line_indent: 0,
1527    };
1528    let raw_value = scrypto_decode::<ScryptoRawValue>(event_data).unwrap();
1529    if let Some(_) = schema_lookup {
1530        write!(
1531            f,
1532            "\n{} Emitter: {}\n   Event: ",
1533            prefix,
1534            emitter.display(receipt_context.address_display_context()),
1535        )?;
1536        format_scrypto_value_with_full_type_id(
1537            f,
1538            print_mode,
1539            raw_value,
1540            receipt_context,
1541            &full_type_id,
1542        )?;
1543    } else {
1544        write!(
1545            f,
1546            "\n{} Emitter: {}\n   Name: {:?}\n   Data: ",
1547            prefix,
1548            emitter.display(receipt_context.address_display_context()),
1549            event_type_identifier.1,
1550        )?;
1551        format_scrypto_value_with_full_type_id(
1552            f,
1553            print_mode,
1554            raw_value,
1555            receipt_context,
1556            &full_type_id,
1557        )?;
1558    }
1559    Ok(())
1560}
1561
1562fn format_scrypto_value_with_full_type_id<'a, F: fmt::Write, T: AsRef<NodeId>>(
1563    f: &mut F,
1564    print_mode: PrintMode,
1565    raw_value: ScryptoRawValue<'_>,
1566    receipt_context: &TransactionReceiptDisplayContext<'a>,
1567    full_type_id: &FullyScopedTypeId<T>,
1568) -> Result<(), fmt::Error> {
1569    let schema_lookup = receipt_context.lookup_schema(full_type_id);
1570    match schema_lookup {
1571        Some((local_type_id, schema)) => format_scrypto_value_with_schema(
1572            f,
1573            print_mode,
1574            raw_value,
1575            receipt_context,
1576            &schema,
1577            local_type_id,
1578        ),
1579        None => {
1580            let display_parameters: ValueDisplayParameters<'_, '_, ScryptoCustomExtension> =
1581                ValueDisplayParameters::Schemaless {
1582                    display_mode: DisplayMode::RustLike(RustLikeOptions::full()),
1583                    print_mode,
1584                    custom_context: receipt_context.display_context(),
1585                    depth_limit: SCRYPTO_SBOR_V1_MAX_DEPTH,
1586                };
1587            write!(f, "{}", raw_value.display(display_parameters))
1588        }
1589    }
1590}
1591
1592fn format_scrypto_value_with_schema<'a, F: fmt::Write>(
1593    f: &mut F,
1594    print_mode: PrintMode,
1595    raw_value: ScryptoRawValue<'_>,
1596    receipt_context: &TransactionReceiptDisplayContext<'a>,
1597    schema: &VersionedScryptoSchema,
1598    local_type_id: LocalTypeId,
1599) -> Result<(), fmt::Error> {
1600    let display_parameters = ValueDisplayParameters::Annotated {
1601        display_mode: DisplayMode::RustLike(RustLikeOptions::full()),
1602        print_mode,
1603        custom_context: receipt_context.display_context(),
1604        schema: schema.v1(),
1605        type_id: local_type_id,
1606        depth_limit: SCRYPTO_SBOR_V1_MAX_DEPTH,
1607    };
1608    write!(f, "{}", raw_value.display(display_parameters))
1609}
1610
1611impl From<FeeReserveFinalizationSummary> for TransactionFeeSummary {
1612    fn from(value: FeeReserveFinalizationSummary) -> Self {
1613        Self {
1614            total_execution_cost_units_consumed: value.total_execution_cost_units_consumed,
1615            total_finalization_cost_units_consumed: value.total_finalization_cost_units_consumed,
1616            total_execution_cost_in_xrd: value.total_execution_cost_in_xrd,
1617            total_finalization_cost_in_xrd: value.total_finalization_cost_in_xrd,
1618            total_tipping_cost_in_xrd: value.total_tipping_cost_in_xrd,
1619            total_storage_cost_in_xrd: value.total_storage_cost_in_xrd,
1620            total_royalty_cost_in_xrd: value.total_royalty_cost_in_xrd,
1621        }
1622    }
1623}
1624
1625impl TransactionFeeSummary {
1626    pub fn total_cost(&self) -> Decimal {
1627        self.total_execution_cost_in_xrd
1628            .checked_add(self.total_finalization_cost_in_xrd)
1629            .unwrap()
1630            .checked_add(self.total_tipping_cost_in_xrd)
1631            .unwrap()
1632            .checked_add(self.total_storage_cost_in_xrd)
1633            .unwrap()
1634            .checked_add(self.total_royalty_cost_in_xrd)
1635            .unwrap()
1636    }
1637
1638    pub fn network_fees(&self) -> Decimal {
1639        self.total_execution_cost_in_xrd
1640            .checked_add(self.total_finalization_cost_in_xrd)
1641            .unwrap()
1642            .checked_add(self.total_storage_cost_in_xrd)
1643            .unwrap()
1644    }
1645
1646    //===================
1647    // For testing only
1648    //===================
1649
1650    pub fn expected_reward_if_single_validator(&self) -> Decimal {
1651        self.expected_reward_as_proposer_if_single_validator()
1652            .checked_add(self.expected_reward_as_active_validator_if_single_validator())
1653            .unwrap()
1654    }
1655
1656    pub fn expected_reward_as_proposer_if_single_validator(&self) -> Decimal {
1657        let one_percent = Decimal::ONE_HUNDREDTH;
1658
1659        one_percent
1660            .checked_mul(TIPS_PROPOSER_SHARE_PERCENTAGE)
1661            .unwrap()
1662            .checked_mul(self.total_tipping_cost_in_xrd)
1663            .unwrap()
1664            .checked_add(
1665                one_percent
1666                    .checked_mul(NETWORK_FEES_PROPOSER_SHARE_PERCENTAGE)
1667                    .unwrap()
1668                    .checked_mul(
1669                        self.total_execution_cost_in_xrd
1670                            .checked_add(self.total_finalization_cost_in_xrd)
1671                            .unwrap()
1672                            .checked_add(self.total_storage_cost_in_xrd)
1673                            .unwrap(),
1674                    )
1675                    .unwrap(),
1676            )
1677            .unwrap()
1678    }
1679
1680    pub fn expected_reward_as_active_validator_if_single_validator(&self) -> Decimal {
1681        let one_percent = Decimal::ONE_HUNDREDTH;
1682
1683        one_percent
1684            .checked_mul(TIPS_VALIDATOR_SET_SHARE_PERCENTAGE)
1685            .unwrap()
1686            .checked_mul(self.total_tipping_cost_in_xrd)
1687            .unwrap()
1688            .checked_add(
1689                one_percent
1690                    .checked_mul(NETWORK_FEES_VALIDATOR_SET_SHARE_PERCENTAGE)
1691                    .unwrap()
1692                    .checked_mul(
1693                        self.total_execution_cost_in_xrd
1694                            .checked_add(self.total_finalization_cost_in_xrd)
1695                            .unwrap()
1696                            .checked_add(self.total_storage_cost_in_xrd)
1697                            .unwrap(),
1698                    )
1699                    .unwrap(),
1700            )
1701            .unwrap()
1702    }
1703}
1704
1705#[cfg(feature = "std")]
1706#[derive(Debug)]
1707pub enum FlamegraphError {
1708    IOError(std::io::Error),
1709    CreationError,
1710    DetailedCostBreakdownNotAvailable,
1711}
1712
1713#[cfg(test)]
1714mod tests {
1715    use radix_transactions::model::TransactionCostingParametersReceiptV2;
1716
1717    use super::*;
1718
1719    define_versioned!(
1720        #[derive(ScryptoSbor)]
1721        VersionedLocalTransactionExecution(LocalTransactionExecutionVersions) {
1722            previous_versions: [
1723                1 => LocalTransactionExecutionV1: { updates_to: 2 },
1724            ],
1725            latest_version: {
1726                2 => LocalTransactionExecution = LocalTransactionExecutionV2,
1727            },
1728        },
1729        outer_attributes: [
1730            // This is an effective copy of the contents of the local transaction execution store in the node.
1731            // This needs to be decodable!
1732            // By all means introduce _new versions_, with conversions between them,
1733            // and we can do the same in the node.
1734            // But this schema can't change, else we won't be able to decode existing executions in the node.
1735            // NOTE: This is just copied here to catch issues / changes earlier; an identical test exists in the node.
1736            #[derive(ScryptoSborAssertion)]
1737            #[sbor_assert(
1738                backwards_compatible(
1739                    bottlenose = "FILE:node_versioned_local_transaction_execution_bottlenose.bin",
1740                    cuttlefish = "FILE:node_versioned_local_transaction_execution_cuttlefish.bin"
1741                ),
1742                settings(allow_name_changes)
1743            )]
1744        ],
1745    );
1746
1747    #[derive(ScryptoSbor)]
1748    struct LocalTransactionExecutionV1 {
1749        outcome: Result<(), ScryptoOwnedRawValue>,
1750        fee_summary: TransactionFeeSummary,
1751        fee_source: FeeSource,
1752        fee_destination: FeeDestination,
1753        engine_costing_parameters: CostingParameters,
1754        transaction_costing_parameters: TransactionCostingParametersReceiptV1,
1755        application_logs: Vec<(Level, String)>,
1756        state_update_summary: StateUpdateSummary,
1757        global_balance_summary: IndexMap<GlobalAddress, IndexMap<ResourceAddress, BalanceChange>>,
1758        substates_system_structure: Vec<SubstateSystemStructure>,
1759        events_system_structure: IndexMap<EventTypeIdentifier, EventSystemStructure>,
1760        next_epoch: Option<EpochChangeEvent>,
1761    }
1762
1763    #[derive(ScryptoSbor)]
1764    struct LocalTransactionExecutionV2 {
1765        outcome: Result<(), PersistableRuntimeError>,
1766        fee_summary: TransactionFeeSummary,
1767        fee_source: FeeSource,
1768        fee_destination: FeeDestination,
1769        engine_costing_parameters: CostingParameters,
1770        transaction_costing_parameters: TransactionCostingParametersReceiptV2,
1771        application_logs: Vec<(Level, String)>,
1772        state_update_summary: StateUpdateSummary,
1773        global_balance_summary: IndexMap<GlobalAddress, IndexMap<ResourceAddress, BalanceChange>>,
1774        substates_system_structure: Vec<SubstateSystemStructure>,
1775        events_system_structure: IndexMap<EventTypeIdentifier, EventSystemStructure>,
1776        next_epoch: Option<EpochChangeEvent>,
1777    }
1778
1779    impl From<LocalTransactionExecutionV1> for LocalTransactionExecutionV2 {
1780        fn from(value: LocalTransactionExecutionV1) -> Self {
1781            Self {
1782                outcome: value.outcome.map_err(|err| PersistableRuntimeError {
1783                    schema_index: 0,
1784                    encoded_error: err,
1785                }),
1786                fee_summary: value.fee_summary,
1787                fee_source: value.fee_source,
1788                fee_destination: value.fee_destination,
1789                engine_costing_parameters: value.engine_costing_parameters,
1790                transaction_costing_parameters: value.transaction_costing_parameters.into(),
1791                application_logs: value.application_logs,
1792                state_update_summary: value.state_update_summary,
1793                global_balance_summary: value.global_balance_summary,
1794                substates_system_structure: value.substates_system_structure,
1795                events_system_structure: value.events_system_structure,
1796                next_epoch: value.next_epoch,
1797            }
1798        }
1799    }
1800}