1use crate::balance::Balance;
5use crate::base_types::SuiAddress;
6use crate::coin::Coin;
7use crate::effects::{
8 AccumulatorOperation, AccumulatorValue, TransactionEffects, TransactionEffectsAPI,
9};
10use crate::full_checkpoint_content::ObjectSet;
11use crate::object::Object;
12use crate::object::Owner;
13use crate::storage::ObjectKey;
14use move_core_types::language_storage::TypeTag;
15
16#[derive(Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord)]
17pub struct BalanceChange {
18 pub address: SuiAddress,
20
21 pub coin_type: TypeTag,
23
24 pub amount: i128,
28}
29
30impl std::fmt::Debug for BalanceChange {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.debug_struct("BalanceChange")
33 .field("address", &self.address)
34 .field("coin_type", &self.coin_type.to_canonical_string(true))
35 .field("amount", &self.amount)
36 .finish()
37 }
38}
39
40fn coins(objects: &[Object]) -> impl Iterator<Item = (&SuiAddress, TypeTag, u64)> + '_ {
41 objects.iter().filter_map(|object| {
42 let address = match object.owner() {
43 Owner::AddressOwner(sui_address)
44 | Owner::ObjectOwner(sui_address)
45 | Owner::ConsensusAddressOwner {
46 owner: sui_address, ..
47 } => sui_address,
48 Owner::Shared { .. } | Owner::Immutable => return None,
49 };
50 let (coin_type, balance) = Coin::extract_balance_if_coin(object).ok().flatten()?;
51 Some((address, coin_type, balance))
52 })
53}
54
55fn signed_balance_change_from_event(
58 event: &crate::accumulator_event::AccumulatorEvent,
59) -> Option<(SuiAddress, TypeTag, i128)> {
60 let ty = &event.write.address.ty;
61 let coin_type = Balance::maybe_get_balance_type_param(ty)?;
63
64 let amount = match &event.write.value {
65 AccumulatorValue::Integer(v) => *v as i128,
66 AccumulatorValue::IntegerTuple(_, _) | AccumulatorValue::EventDigest(_) => {
68 return None;
69 }
70 };
71
72 let signed_amount = match event.write.operation {
74 AccumulatorOperation::Split => -amount,
75 AccumulatorOperation::Merge => amount,
76 };
77
78 Some((event.write.address.address, coin_type, signed_amount))
79}
80
81pub fn signed_balance_changes_from_events(
83 events: &[crate::accumulator_event::AccumulatorEvent],
84) -> impl Iterator<Item = (SuiAddress, TypeTag, i128)> + '_ {
85 events.iter().filter_map(signed_balance_change_from_event)
86}
87
88pub fn address_balance_changes_from_accumulator_events(
90 effects: &TransactionEffects,
91) -> impl Iterator<Item = (SuiAddress, TypeTag, i128)> {
92 effects
93 .accumulator_events()
94 .into_iter()
95 .filter_map(|ref event| signed_balance_change_from_event(event))
96}
97
98pub fn derive_balance_changes(
99 effects: &TransactionEffects,
100 input_objects: &[Object],
101 output_objects: &[Object],
102) -> Vec<BalanceChange> {
103 let balances = coins(input_objects).fold(
105 std::collections::BTreeMap::<_, i128>::new(),
106 |mut acc, (address, coin_type, balance)| {
107 *acc.entry((*address, coin_type)).or_default() -= balance as i128;
108 acc
109 },
110 );
111
112 let balances =
114 coins(output_objects).fold(balances, |mut acc, (address, coin_type, balance)| {
115 *acc.entry((*address, coin_type)).or_default() += balance as i128;
116 acc
117 });
118
119 let balances = address_balance_changes_from_accumulator_events(effects).fold(
121 balances,
122 |mut acc, (address, coin_type, signed_amount)| {
123 *acc.entry((address, coin_type)).or_default() += signed_amount;
124 acc
125 },
126 );
127
128 balances
129 .into_iter()
130 .filter_map(|((address, coin_type), amount)| {
131 if amount == 0 {
132 return None;
133 }
134
135 Some(BalanceChange {
136 address,
137 coin_type,
138 amount,
139 })
140 })
141 .collect()
142}
143
144pub fn derive_balance_changes_2(
145 effects: &TransactionEffects,
146 objects: &ObjectSet,
147) -> Vec<BalanceChange> {
148 let input_objects = effects
149 .modified_at_versions()
150 .into_iter()
151 .filter_map(|(object_id, version)| objects.get(&ObjectKey(object_id, version)).cloned())
152 .collect::<Vec<_>>();
153 let output_objects = effects
154 .all_changed_objects()
155 .into_iter()
156 .filter_map(|(object_ref, _owner, _kind)| objects.get(&object_ref.into()).cloned())
157 .collect::<Vec<_>>();
158
159 let balances = coins(&input_objects).fold(
161 std::collections::BTreeMap::<_, i128>::new(),
162 |mut acc, (address, coin_type, balance)| {
163 *acc.entry((*address, coin_type)).or_default() -= balance as i128;
164 acc
165 },
166 );
167
168 let balances =
170 coins(&output_objects).fold(balances, |mut acc, (address, coin_type, balance)| {
171 *acc.entry((*address, coin_type)).or_default() += balance as i128;
172 acc
173 });
174
175 let balances = address_balance_changes_from_accumulator_events(effects).fold(
177 balances,
178 |mut acc, (address, coin_type, signed_amount)| {
179 *acc.entry((address, coin_type)).or_default() += signed_amount;
180 acc
181 },
182 );
183
184 balances
185 .into_iter()
186 .filter_map(|((address, coin_type), amount)| {
187 if amount == 0 {
188 return None;
189 }
190
191 Some(BalanceChange {
192 address,
193 coin_type,
194 amount,
195 })
196 })
197 .collect()
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::accumulator_root::AccumulatorValue as AccumulatorValueRoot;
204 use crate::balance::Balance;
205 use crate::base_types::ObjectID;
206 use crate::digests::TransactionDigest;
207 use crate::effects::{
208 AccumulatorAddress, AccumulatorOperation, AccumulatorValue, AccumulatorWriteV1,
209 EffectsObjectChange, IDOperation, ObjectIn, ObjectOut, TransactionEffects,
210 };
211 use crate::execution_status::ExecutionStatus;
212 use crate::gas::GasCostSummary;
213 use move_core_types::language_storage::TypeTag;
214
215 fn create_effects_with_accumulator_writes(
216 writes: Vec<(ObjectID, AccumulatorWriteV1)>,
217 ) -> TransactionEffects {
218 let changed_objects = writes
219 .into_iter()
220 .map(|(id, write)| {
221 (
222 id,
223 EffectsObjectChange {
224 input_state: ObjectIn::NotExist,
225 output_state: ObjectOut::AccumulatorWriteV1(write),
226 id_operation: IDOperation::None,
227 },
228 )
229 })
230 .collect();
231
232 TransactionEffects::new_from_execution_v2(
233 ExecutionStatus::Success,
234 0,
235 GasCostSummary::default(),
236 vec![],
237 std::collections::BTreeSet::new(),
238 TransactionDigest::random(),
239 crate::base_types::SequenceNumber::new(),
240 changed_objects,
241 None,
242 None,
243 vec![],
244 )
245 }
246
247 fn sui_balance_type() -> TypeTag {
248 Balance::type_tag("0x2::sui::SUI".parse().unwrap())
249 }
250
251 fn custom_coin_type() -> TypeTag {
252 "0xabc::my_coin::MY_COIN".parse().unwrap()
253 }
254
255 fn custom_balance_type() -> TypeTag {
256 Balance::type_tag(custom_coin_type())
257 }
258
259 fn get_accumulator_obj_id(address: SuiAddress, balance_type: &TypeTag) -> ObjectID {
260 *AccumulatorValueRoot::get_field_id(address, balance_type)
261 .unwrap()
262 .inner()
263 }
264
265 #[test]
266 fn test_derive_balance_changes_with_no_accumulator_events() {
267 let effects = create_effects_with_accumulator_writes(vec![]);
268 let result = derive_balance_changes(&effects, &[], &[]);
269 assert!(result.is_empty());
270 }
271
272 #[test]
273 fn test_derive_balance_changes_with_split_accumulator_event() {
274 let address = SuiAddress::random_for_testing_only();
275 let balance_type = sui_balance_type();
276 let obj_id = get_accumulator_obj_id(address, &balance_type);
277 let write = AccumulatorWriteV1 {
278 address: AccumulatorAddress::new(address, balance_type),
279 operation: AccumulatorOperation::Split,
280 value: AccumulatorValue::Integer(1000),
281 };
282 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
283
284 let result = derive_balance_changes(&effects, &[], &[]);
285
286 assert_eq!(result.len(), 1);
287 assert_eq!(result[0].address, address);
288 assert_eq!(
289 result[0].coin_type,
290 "0x2::sui::SUI".parse::<TypeTag>().unwrap()
291 );
292 assert_eq!(result[0].amount, -1000);
293 }
294
295 #[test]
296 fn test_derive_balance_changes_with_merge_accumulator_event() {
297 let address = SuiAddress::random_for_testing_only();
298 let balance_type = sui_balance_type();
299 let obj_id = get_accumulator_obj_id(address, &balance_type);
300 let write = AccumulatorWriteV1 {
301 address: AccumulatorAddress::new(address, balance_type),
302 operation: AccumulatorOperation::Merge,
303 value: AccumulatorValue::Integer(500),
304 };
305 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
306
307 let result = derive_balance_changes(&effects, &[], &[]);
308
309 assert_eq!(result.len(), 1);
310 assert_eq!(result[0].address, address);
311 assert_eq!(result[0].amount, 500);
312 }
313
314 #[test]
315 fn test_derive_balance_changes_with_multiple_addresses() {
316 let address1 = SuiAddress::random_for_testing_only();
317 let address2 = SuiAddress::random_for_testing_only();
318 let balance_type = sui_balance_type();
319
320 let obj_id1 = get_accumulator_obj_id(address1, &balance_type);
321 let obj_id2 = get_accumulator_obj_id(address2, &balance_type);
322
323 let write1 = AccumulatorWriteV1 {
324 address: AccumulatorAddress::new(address1, balance_type.clone()),
325 operation: AccumulatorOperation::Split,
326 value: AccumulatorValue::Integer(1000),
327 };
328 let write2 = AccumulatorWriteV1 {
329 address: AccumulatorAddress::new(address2, balance_type),
330 operation: AccumulatorOperation::Merge,
331 value: AccumulatorValue::Integer(1000),
332 };
333
334 let effects =
335 create_effects_with_accumulator_writes(vec![(obj_id1, write1), (obj_id2, write2)]);
336
337 let result = derive_balance_changes(&effects, &[], &[]);
338
339 assert_eq!(result.len(), 2);
340 let addr1_change = result.iter().find(|c| c.address == address1).unwrap();
341 let addr2_change = result.iter().find(|c| c.address == address2).unwrap();
342 assert_eq!(addr1_change.amount, -1000);
343 assert_eq!(addr2_change.amount, 1000);
344 }
345
346 #[test]
347 fn test_derive_balance_changes_with_custom_coin_type() {
348 let address = SuiAddress::random_for_testing_only();
349 let balance_type = custom_balance_type();
350 let obj_id = get_accumulator_obj_id(address, &balance_type);
351 let write = AccumulatorWriteV1 {
352 address: AccumulatorAddress::new(address, balance_type),
353 operation: AccumulatorOperation::Split,
354 value: AccumulatorValue::Integer(2000),
355 };
356 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
357
358 let result = derive_balance_changes(&effects, &[], &[]);
359
360 assert_eq!(result.len(), 1);
361 assert_eq!(result[0].address, address);
362 assert_eq!(result[0].coin_type, custom_coin_type());
363 assert_eq!(result[0].amount, -2000);
364 }
365
366 #[test]
367 fn test_derive_balance_changes_ignores_non_balance_types() {
368 let address = SuiAddress::random_for_testing_only();
369 let non_balance_type: TypeTag = "0x2::accumulator_settlement::EventStreamHead"
371 .parse()
372 .unwrap();
373 let write = AccumulatorWriteV1 {
374 address: AccumulatorAddress::new(address, non_balance_type),
375 operation: AccumulatorOperation::Split,
376 value: AccumulatorValue::Integer(1000),
377 };
378 let effects = create_effects_with_accumulator_writes(vec![(ObjectID::random(), write)]);
379
380 let result = derive_balance_changes(&effects, &[], &[]);
381
382 assert!(result.is_empty());
383 }
384
385 #[test]
386 fn test_derive_balance_changes_ignores_event_digest_values() {
387 use crate::digests::Digest;
388 use nonempty::nonempty;
389
390 let address = SuiAddress::random_for_testing_only();
391 let balance_type = sui_balance_type();
392 let obj_id = get_accumulator_obj_id(address, &balance_type);
393 let write = AccumulatorWriteV1 {
394 address: AccumulatorAddress::new(address, balance_type),
395 operation: AccumulatorOperation::Merge,
396 value: AccumulatorValue::EventDigest(nonempty![(0, Digest::random())]),
397 };
398 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
399
400 let result = derive_balance_changes(&effects, &[], &[]);
401
402 assert!(result.is_empty());
403 }
404
405 #[test]
406 fn test_derive_balance_changes_accumulator_zero_amount_filtered() {
407 let address = SuiAddress::random_for_testing_only();
409 let balance_type = sui_balance_type();
410 let obj_id = get_accumulator_obj_id(address, &balance_type);
411
412 let write = AccumulatorWriteV1 {
413 address: AccumulatorAddress::new(address, balance_type),
414 operation: AccumulatorOperation::Split,
415 value: AccumulatorValue::Integer(0),
416 };
417 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
418
419 let result = derive_balance_changes(&effects, &[], &[]);
420
421 assert!(result.is_empty());
423 }
424
425 #[test]
426 fn test_derive_balance_changes_2_with_accumulator_events() {
427 let address = SuiAddress::random_for_testing_only();
428 let balance_type = sui_balance_type();
429 let obj_id = get_accumulator_obj_id(address, &balance_type);
430 let write = AccumulatorWriteV1 {
431 address: AccumulatorAddress::new(address, balance_type),
432 operation: AccumulatorOperation::Split,
433 value: AccumulatorValue::Integer(1000),
434 };
435 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
436
437 let objects = crate::full_checkpoint_content::ObjectSet::default();
438 let result = derive_balance_changes_2(&effects, &objects);
439
440 assert_eq!(result.len(), 1);
441 assert_eq!(result[0].address, address);
442 assert_eq!(
443 result[0].coin_type,
444 "0x2::sui::SUI".parse::<TypeTag>().unwrap()
445 );
446 assert_eq!(result[0].amount, -1000);
447 }
448
449 fn create_gas_coin_object(owner: SuiAddress, value: u64) -> Object {
452 use crate::base_types::SequenceNumber;
453 use crate::object::MoveObject;
454
455 let obj_id = ObjectID::random();
456 let move_obj = MoveObject::new_gas_coin(SequenceNumber::new(), obj_id, value);
457 Object::new_move(
458 move_obj,
459 Owner::AddressOwner(owner),
460 TransactionDigest::random(),
461 )
462 }
463
464 fn create_custom_coin_object(owner: SuiAddress, coin_type: TypeTag, value: u64) -> Object {
465 use crate::base_types::SequenceNumber;
466 use crate::object::MoveObject;
467
468 let obj_id = ObjectID::random();
469 let move_obj = MoveObject::new_coin(coin_type, SequenceNumber::new(), obj_id, value);
470 Object::new_move(
471 move_obj,
472 Owner::AddressOwner(owner),
473 TransactionDigest::random(),
474 )
475 }
476
477 #[test]
478 fn test_derive_balance_changes_with_coin_objects_only() {
479 let address = SuiAddress::random_for_testing_only();
480
481 let input_coin = create_gas_coin_object(address, 5000);
483 let output_coin = create_gas_coin_object(address, 3000);
485
486 let effects = create_effects_with_accumulator_writes(vec![]);
487
488 let result = derive_balance_changes(&effects, &[input_coin], &[output_coin]);
489
490 assert_eq!(result.len(), 1);
491 assert_eq!(result[0].address, address);
492 assert_eq!(result[0].amount, -2000); }
494
495 #[test]
496 fn test_derive_balance_changes_coin_transfer_between_addresses() {
497 let sender = SuiAddress::random_for_testing_only();
498 let receiver = SuiAddress::random_for_testing_only();
499
500 let input_coin = create_gas_coin_object(sender, 10000);
502 let output_coin_sender = create_gas_coin_object(sender, 7000);
504 let output_coin_receiver = create_gas_coin_object(receiver, 3000);
505
506 let effects = create_effects_with_accumulator_writes(vec![]);
507
508 let result = derive_balance_changes(
509 &effects,
510 &[input_coin],
511 &[output_coin_sender, output_coin_receiver],
512 );
513
514 assert_eq!(result.len(), 2);
515 let sender_change = result.iter().find(|c| c.address == sender).unwrap();
516 let receiver_change = result.iter().find(|c| c.address == receiver).unwrap();
517 assert_eq!(sender_change.amount, -3000); assert_eq!(receiver_change.amount, 3000); }
520
521 #[test]
522 fn test_derive_balance_changes_combines_coins_and_accumulator_events() {
523 let address = SuiAddress::random_for_testing_only();
524 let balance_type = sui_balance_type();
525 let obj_id = get_accumulator_obj_id(address, &balance_type);
526
527 let input_coin = create_gas_coin_object(address, 5000);
529 let output_coin = create_gas_coin_object(address, 3000);
530
531 let write = AccumulatorWriteV1 {
533 address: AccumulatorAddress::new(address, balance_type),
534 operation: AccumulatorOperation::Merge,
535 value: AccumulatorValue::Integer(500),
536 };
537 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
538
539 let result = derive_balance_changes(&effects, &[input_coin], &[output_coin]);
540
541 assert_eq!(result.len(), 1);
543 assert_eq!(result[0].address, address);
544 assert_eq!(result[0].amount, -1500);
545 }
546
547 #[test]
548 fn test_derive_balance_changes_coins_and_accumulator_different_addresses() {
549 let coin_owner = SuiAddress::random_for_testing_only();
550 let accumulator_owner = SuiAddress::random_for_testing_only();
551 let balance_type = sui_balance_type();
552 let obj_id = get_accumulator_obj_id(accumulator_owner, &balance_type);
553
554 let input_coin = create_gas_coin_object(coin_owner, 5000);
556 let output_coin = create_gas_coin_object(coin_owner, 4000);
557
558 let write = AccumulatorWriteV1 {
560 address: AccumulatorAddress::new(accumulator_owner, balance_type),
561 operation: AccumulatorOperation::Merge,
562 value: AccumulatorValue::Integer(2000),
563 };
564 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
565
566 let result = derive_balance_changes(&effects, &[input_coin], &[output_coin]);
567
568 assert_eq!(result.len(), 2);
569 let coin_change = result.iter().find(|c| c.address == coin_owner).unwrap();
570 let acc_change = result
571 .iter()
572 .find(|c| c.address == accumulator_owner)
573 .unwrap();
574 assert_eq!(coin_change.amount, -1000);
575 assert_eq!(acc_change.amount, 2000);
576 }
577
578 #[test]
579 fn test_derive_balance_changes_coins_and_accumulator_net_to_zero() {
580 let address = SuiAddress::random_for_testing_only();
581 let balance_type = sui_balance_type();
582 let obj_id = get_accumulator_obj_id(address, &balance_type);
583
584 let input_coin = create_gas_coin_object(address, 5000);
586 let output_coin = create_gas_coin_object(address, 4000);
587
588 let write = AccumulatorWriteV1 {
590 address: AccumulatorAddress::new(address, balance_type),
591 operation: AccumulatorOperation::Merge,
592 value: AccumulatorValue::Integer(1000),
593 };
594 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
595
596 let result = derive_balance_changes(&effects, &[input_coin], &[output_coin]);
597
598 assert!(result.is_empty());
600 }
601
602 #[test]
603 fn test_derive_balance_changes_different_coin_types() {
604 let address = SuiAddress::random_for_testing_only();
605 let custom_type = custom_coin_type();
606 let custom_balance = custom_balance_type();
607 let obj_id = get_accumulator_obj_id(address, &custom_balance);
608
609 let sui_input = create_gas_coin_object(address, 5000);
611 let sui_output = create_gas_coin_object(address, 4000);
612
613 let custom_output = create_custom_coin_object(address, custom_type.clone(), 500);
615
616 let write = AccumulatorWriteV1 {
618 address: AccumulatorAddress::new(address, custom_balance),
619 operation: AccumulatorOperation::Merge,
620 value: AccumulatorValue::Integer(300),
621 };
622 let effects = create_effects_with_accumulator_writes(vec![(obj_id, write)]);
623
624 let result = derive_balance_changes(&effects, &[sui_input], &[sui_output, custom_output]);
625
626 assert_eq!(result.len(), 2);
627
628 let sui_change = result
629 .iter()
630 .find(|c| c.coin_type == "0x2::sui::SUI".parse::<TypeTag>().unwrap())
631 .unwrap();
632 let custom_change = result.iter().find(|c| c.coin_type == custom_type).unwrap();
633
634 assert_eq!(sui_change.amount, -1000);
635 assert_eq!(custom_change.amount, 800); }
637
638 #[test]
639 fn test_derive_balance_changes_accumulator_split_with_coins() {
640 let sender = SuiAddress::random_for_testing_only();
641 let receiver = SuiAddress::random_for_testing_only();
642 let balance_type = sui_balance_type();
643 let sender_obj_id = get_accumulator_obj_id(sender, &balance_type);
644 let receiver_obj_id = get_accumulator_obj_id(receiver, &balance_type.clone());
645
646 let input_coin = create_gas_coin_object(sender, 5000);
648 let output_coin = create_gas_coin_object(sender, 4000);
649
650 let sender_write = AccumulatorWriteV1 {
652 address: AccumulatorAddress::new(sender, balance_type.clone()),
653 operation: AccumulatorOperation::Split,
654 value: AccumulatorValue::Integer(500),
655 };
656 let receiver_write = AccumulatorWriteV1 {
658 address: AccumulatorAddress::new(receiver, balance_type),
659 operation: AccumulatorOperation::Merge,
660 value: AccumulatorValue::Integer(500),
661 };
662 let effects = create_effects_with_accumulator_writes(vec![
663 (sender_obj_id, sender_write),
664 (receiver_obj_id, receiver_write),
665 ]);
666
667 let result = derive_balance_changes(&effects, &[input_coin], &[output_coin]);
668
669 assert_eq!(result.len(), 2);
670 let sender_change = result.iter().find(|c| c.address == sender).unwrap();
671 let receiver_change = result.iter().find(|c| c.address == receiver).unwrap();
672
673 assert_eq!(sender_change.amount, -1500);
675 assert_eq!(receiver_change.amount, 500);
677 }
678}