radix_engine/transaction/
state_update_summary.rs

1use crate::blueprints::resource::{FungibleVaultBalanceFieldPayload, FungibleVaultField};
2use crate::internal_prelude::*;
3use crate::system::system_db_reader::SystemDatabaseReader;
4use radix_common::data::scrypto::model::*;
5use radix_common::math::*;
6use radix_engine_interface::types::*;
7use radix_substate_store_interface::interface::*;
8use sbor::rust::prelude::*;
9
10#[derive(Default, Debug, Clone, ScryptoSbor, PartialEq, Eq)]
11pub struct StateUpdateSummary {
12    pub new_packages: IndexSet<PackageAddress>,
13    pub new_components: IndexSet<ComponentAddress>,
14    pub new_resources: IndexSet<ResourceAddress>,
15    pub new_vaults: IndexSet<InternalAddress>,
16    pub vault_balance_changes: IndexMap<NodeId, (ResourceAddress, BalanceChange)>,
17}
18
19impl StateUpdateSummary {
20    pub fn new<S: SubstateDatabase>(
21        substate_db: &S,
22        new_node_ids: IndexSet<NodeId>,
23        updates: &StateUpdates,
24    ) -> Self {
25        let mut new_packages = index_set_new();
26        let mut new_components = index_set_new();
27        let mut new_resources = index_set_new();
28        let mut new_vaults = index_set_new();
29
30        for node_id in new_node_ids {
31            if node_id.is_global_package() {
32                new_packages.insert(PackageAddress::new_or_panic(node_id.0));
33            }
34            if node_id.is_global_component() {
35                new_components.insert(ComponentAddress::new_or_panic(node_id.0));
36            }
37            if node_id.is_global_resource_manager() {
38                new_resources.insert(ResourceAddress::new_or_panic(node_id.0));
39            }
40            if node_id.is_internal_vault() {
41                new_vaults.insert(InternalAddress::new_or_panic(node_id.0));
42            }
43        }
44
45        let vault_balance_changes = BalanceAccounter::new(substate_db, &updates).run();
46
47        StateUpdateSummary {
48            new_packages,
49            new_components,
50            new_resources,
51            new_vaults,
52            vault_balance_changes,
53        }
54    }
55
56    pub fn new_from_state_updates_on_db(
57        base_substate_db: &impl SubstateDatabase,
58        updates: &StateUpdates,
59    ) -> Self {
60        let mut new_packages = index_set_new();
61        let mut new_components = index_set_new();
62        let mut new_resources = index_set_new();
63        let mut new_vaults = index_set_new();
64
65        let new_node_ids = updates
66            .by_node
67            .iter()
68            .filter(|(node_id, updates)| {
69                let type_id_partition_number = TYPE_INFO_FIELD_PARTITION;
70                let type_id_substate_key = TypeInfoField::TypeInfo.into();
71                let possible_creation = updates
72                    .of_partition_ref(type_id_partition_number)
73                    .is_some_and(|partition_updates| {
74                        partition_updates.contains_set_update_for(&type_id_substate_key)
75                    });
76                if !possible_creation {
77                    return false;
78                }
79                let node_previously_existed = base_substate_db
80                    .get_raw_substate(node_id, type_id_partition_number, type_id_substate_key)
81                    .is_some();
82                return !node_previously_existed;
83            })
84            .map(|(node_id, _)| node_id);
85
86        for node_id in new_node_ids {
87            if node_id.is_global_package() {
88                new_packages.insert(PackageAddress::new_or_panic(node_id.0));
89            }
90            if node_id.is_global_component() {
91                new_components.insert(ComponentAddress::new_or_panic(node_id.0));
92            }
93            if node_id.is_global_resource_manager() {
94                new_resources.insert(ResourceAddress::new_or_panic(node_id.0));
95            }
96            if node_id.is_internal_vault() {
97                new_vaults.insert(InternalAddress::new_or_panic(node_id.0));
98            }
99        }
100
101        let vault_balance_changes = BalanceAccounter::new(base_substate_db, &updates).run();
102
103        StateUpdateSummary {
104            new_packages,
105            new_components,
106            new_resources,
107            new_vaults,
108            vault_balance_changes,
109        }
110    }
111}
112
113#[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)]
114pub enum BalanceChange {
115    Fungible(Decimal),
116    NonFungible {
117        added: BTreeSet<NonFungibleLocalId>,
118        removed: BTreeSet<NonFungibleLocalId>,
119    },
120}
121
122impl AddAssign for BalanceChange {
123    fn add_assign(&mut self, rhs: Self) {
124        match self {
125            BalanceChange::Fungible(self_value) => {
126                let BalanceChange::Fungible(value) = rhs else {
127                    panic!("cannot {:?} + {:?}", self, rhs);
128                };
129                *self_value = self_value.checked_add(value).unwrap();
130            }
131            BalanceChange::NonFungible {
132                added: self_added,
133                removed: self_removed,
134            } => {
135                let BalanceChange::NonFungible { added, removed } = rhs else {
136                    panic!("cannot {:?} + {:?}", self, rhs);
137                };
138
139                for remove in removed {
140                    if !self_added.remove(&remove) {
141                        self_removed.insert(remove);
142                    }
143                }
144
145                for add in added {
146                    if !self_removed.remove(&add) {
147                        self_added.insert(add);
148                    }
149                }
150            }
151        }
152    }
153}
154
155impl BalanceChange {
156    pub fn prune_and_check_if_zero(&mut self) -> bool {
157        match self {
158            BalanceChange::Fungible(x) => x.is_zero(),
159            BalanceChange::NonFungible { added, removed } => {
160                let cancelled_out = added
161                    .intersection(&removed)
162                    .cloned()
163                    .collect::<BTreeSet<_>>();
164                added.retain(|id| !cancelled_out.contains(id));
165                removed.retain(|id| !cancelled_out.contains(id));
166
167                added.is_empty() && removed.is_empty()
168            }
169        }
170    }
171
172    pub fn fungible(&mut self) -> &mut Decimal {
173        match self {
174            BalanceChange::Fungible(x) => x,
175            BalanceChange::NonFungible { .. } => panic!("Not fungible"),
176        }
177    }
178    pub fn added_non_fungibles(&mut self) -> &mut BTreeSet<NonFungibleLocalId> {
179        match self {
180            BalanceChange::Fungible(..) => panic!("Not non fungible"),
181            BalanceChange::NonFungible { added, .. } => added,
182        }
183    }
184    pub fn removed_non_fungibles(&mut self) -> &mut BTreeSet<NonFungibleLocalId> {
185        match self {
186            BalanceChange::Fungible(..) => panic!("Not non fungible"),
187            BalanceChange::NonFungible { removed, .. } => removed,
188        }
189    }
190}
191
192/// Note that the implementation below assumes that substate owned objects can not be
193/// detached. If this changes, we will have to account for objects that are removed
194/// from a substate.
195pub struct BalanceAccounter<'a, S: SubstateDatabase> {
196    system_reader: SystemDatabaseReader<'a, S>,
197    state_updates: &'a StateUpdates,
198}
199
200impl<'a, S: SubstateDatabase> BalanceAccounter<'a, S> {
201    pub fn new(substate_db: &'a S, state_updates: &'a StateUpdates) -> Self {
202        Self {
203            system_reader: SystemDatabaseReader::new_with_overlay(substate_db, state_updates),
204            state_updates,
205        }
206    }
207
208    pub fn run(&self) -> IndexMap<NodeId, (ResourceAddress, BalanceChange)> {
209        self.state_updates
210            .by_node
211            .keys()
212            .filter(|node_id| node_id.is_internal_vault())
213            .filter_map(|vault_id| {
214                self.calculate_vault_balance_change(vault_id)
215                    .map(|change| (*vault_id, change))
216            })
217            .collect::<IndexMap<_, _>>()
218    }
219
220    fn calculate_vault_balance_change(
221        &self,
222        vault_id: &NodeId,
223    ) -> Option<(ResourceAddress, BalanceChange)> {
224        let object_info = self
225            .system_reader
226            .get_object_info(*vault_id)
227            .expect("Missing vault info");
228
229        let resource_address = ResourceAddress::new_or_panic(object_info.get_outer_object().into());
230
231        let change = if resource_address.is_fungible() {
232            self.calculate_fungible_vault_balance_change(vault_id)
233        } else {
234            self.calculate_non_fungible_vault_balance_change(vault_id)
235        };
236
237        change.map(|change| (resource_address, change))
238    }
239
240    fn calculate_fungible_vault_balance_change(&self, vault_id: &NodeId) -> Option<BalanceChange> {
241        self
242            .system_reader
243            .fetch_substate::<FieldSubstate<FungibleVaultBalanceFieldPayload>>(
244                vault_id,
245                MAIN_BASE_PARTITION,
246                &FungibleVaultField::Balance.into(),
247            )
248            .map(|new_substate| new_substate.into_payload().fully_update_and_into_latest_version().amount())
249            .map(|new_balance| {
250                let old_balance = self
251                    .system_reader
252                    .fetch_substate_from_database::<FieldSubstate<FungibleVaultBalanceFieldPayload>>(
253                        vault_id,
254                        MAIN_BASE_PARTITION,
255                        &FungibleVaultField::Balance.into(),
256                    )
257                    .map(|old_balance| old_balance.into_payload().fully_update_and_into_latest_version().amount())
258                    .unwrap_or(Decimal::ZERO);
259
260                // TODO: Handle potential Decimal arithmetic operation (safe_sub) errors instead of panicking.
261                new_balance.checked_sub(old_balance).unwrap()
262            })
263            .filter(|change| change != &Decimal::ZERO) // prune
264            .map(|change| BalanceChange::Fungible(change))
265    }
266
267    fn calculate_non_fungible_vault_balance_change(
268        &self,
269        vault_id: &NodeId,
270    ) -> Option<BalanceChange> {
271        let partition_num = MAIN_BASE_PARTITION.at_offset(PartitionOffset(1u8)).unwrap();
272
273        self.state_updates
274            .by_node
275            .get(vault_id)
276            .map(|node_updates| match node_updates {
277                NodeStateUpdates::Delta { by_partition } => by_partition,
278            })
279            .and_then(|partitions| partitions.get(&partition_num))
280            .map(|partition_update| {
281                let mut added = BTreeSet::new();
282                let mut removed = BTreeSet::new();
283
284                match partition_update {
285                    PartitionStateUpdates::Delta { by_substate } => {
286                        for (substate_key, substate_update) in by_substate {
287                            let id: NonFungibleLocalId =
288                                scrypto_decode(substate_key.for_map().unwrap()).unwrap();
289                            let previous_value = self
290                                .system_reader
291                                .fetch_substate_from_database::<ScryptoValue>(
292                                    vault_id,
293                                    partition_num,
294                                    substate_key,
295                                );
296
297                            match substate_update {
298                                DatabaseUpdate::Set(_) => {
299                                    if previous_value.is_none() {
300                                        added.insert(id);
301                                    }
302                                }
303                                DatabaseUpdate::Delete => {
304                                    if previous_value.is_some() {
305                                        removed.insert(id);
306                                    }
307                                }
308                            }
309                        }
310                    }
311                    PartitionStateUpdates::Batch(_) => {
312                        panic!("Invariant: vault partitions are never batch removed")
313                    }
314                }
315
316                (added, removed)
317            })
318            .map(|(mut added, mut removed)| {
319                // prune
320                let cancelled_out = added
321                    .intersection(&removed)
322                    .cloned()
323                    .collect::<BTreeSet<_>>();
324                added.retain(|id| !cancelled_out.contains(id));
325                removed.retain(|id| !cancelled_out.contains(id));
326                (added, removed)
327            })
328            .filter(|(added, removed)| !added.is_empty() || !removed.is_empty())
329            .map(|(added, removed)| BalanceChange::NonFungible { added, removed })
330    }
331}