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