radix_engine/transaction/
state_update_summary.rs1use 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
192pub 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 new_balance.checked_sub(old_balance).unwrap()
262 })
263 .filter(|change| change != &Decimal::ZERO) .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 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}