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