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