1pub use checked::*;
6
7#[sui_macros::with_checked_arithmetic]
8pub mod checked {
9
10 use crate::sui_types::gas::SuiGasStatusAPI;
11 use crate::temporary_store::TemporaryStore;
12 use either::Either;
13 use indexmap::IndexMap;
14 use mysten_common::assert_reachable;
15 use sui_protocol_config::ProtocolConfig;
16 use sui_types::deny_list_v2::CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
17 use sui_types::digests::TransactionDigest;
18 use sui_types::error::ExecutionErrorTrait;
19 use sui_types::gas::{GasCostSummary, SuiGasStatus, deduct_gas};
20 use sui_types::gas_model::gas_predicates::refresh_gas_payment_location;
21 use sui_types::{
22 accumulator_event::AccumulatorEvent,
23 base_types::{ObjectID, ObjectRef, SuiAddress},
24 error::ExecutionError,
25 gas_model::tables::GasStatus,
26 is_system_package,
27 object::Data,
28 };
29 use tracing::trace;
30
31 #[derive(Debug)]
36 pub struct GasCharger {
37 tx_digest: TransactionDigest,
38 gas_model_version: u64,
39 payment: PaymentMetadata,
40 gas_status: SuiGasStatus,
41 }
42
43 #[derive(Debug)]
49 enum PaymentMetadata {
50 Unmetered,
51 Gasless,
52 Smash(SmashMetadata),
54 }
55
56 #[derive(Debug)]
62 struct SmashMetadata {
63 gas_charge_location: PaymentLocation,
66 total_smashed: u64,
68 smash_target: PaymentMethod,
71 smashed_payments: IndexMap<PaymentLocation, PaymentMethod>,
74 }
75
76 #[derive(Debug)]
80 pub struct PaymentKind(PaymentKind_);
81
82 #[derive(Debug)]
85 enum PaymentKind_ {
86 Unmetered,
87 Gasless,
88 Smash(IndexMap<PaymentLocation, PaymentMethod>),
91 }
92
93 #[derive(Debug)]
96 pub enum PaymentMethod {
97 Coin(ObjectRef),
98 AddressBalance(SuiAddress, u64),
99 }
100
101 #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
104 pub enum PaymentLocation {
105 Coin(ObjectID),
106 AddressBalance(SuiAddress),
107 }
108
109 #[derive(Debug, Clone, Copy)]
114 pub struct GasPayment {
115 pub location: PaymentLocation,
118 pub amount: u64,
120 }
121
122 impl GasCharger {
123 pub fn new(
124 tx_digest: TransactionDigest,
125 payment_kind: PaymentKind,
126 gas_status: SuiGasStatus,
127 temporary_store: &mut TemporaryStore<'_>,
128 protocol_config: &ProtocolConfig,
129 ) -> Self {
130 let gas_model_version = protocol_config.gas_model_version();
131 let payment = match payment_kind.0 {
132 PaymentKind_::Unmetered => PaymentMetadata::Unmetered,
133 PaymentKind_::Gasless => PaymentMetadata::Gasless,
134 PaymentKind_::Smash(mut payment_methods) => {
135 let (_, smash_target) = payment_methods.shift_remove_index(0).unwrap();
136 let mut metadata = SmashMetadata {
137 total_smashed: 0,
139 gas_charge_location: smash_target.location(),
140 smash_target,
141 smashed_payments: payment_methods,
142 };
143 metadata.smash_gas(&tx_digest, temporary_store);
144 PaymentMetadata::Smash(metadata)
145 }
146 };
147 Self {
148 tx_digest,
149 gas_model_version,
150 payment,
151 gas_status,
152 }
153 }
154
155 pub fn new_unmetered(
156 tx_digest: TransactionDigest,
157 protocol_config: &ProtocolConfig,
158 ) -> Self {
159 Self {
160 tx_digest,
161 gas_model_version: protocol_config.gas_model_version(),
162 payment: PaymentMetadata::Unmetered,
163 gas_status: SuiGasStatus::new_unmetered(protocol_config),
164 }
165 }
166
167 pub(crate) fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
170 match &self.payment {
171 PaymentMetadata::Unmetered | PaymentMetadata::Gasless => {
172 Either::Left(std::iter::empty())
173 }
174 PaymentMetadata::Smash(metadata) => Either::Right(metadata.used_coins()),
175 }
176 }
177
178 pub fn override_gas_charge_location(
180 &mut self,
181 location: PaymentLocation,
182 ) -> Result<(), ExecutionError> {
183 if let PaymentMetadata::Smash(metadata) = &mut self.payment {
184 metadata.gas_charge_location = location;
185 Ok(())
186 } else {
187 invariant_violation!("Can only override gas charge location in the smash-gas case")
188 }
189 }
190
191 pub fn gas_payment_amount(&self) -> Option<GasPayment> {
198 match &self.payment {
199 PaymentMetadata::Unmetered | PaymentMetadata::Gasless => None,
200 PaymentMetadata::Smash(metadata) => Some(GasPayment {
201 location: metadata.smash_target.location(),
202 amount: metadata.total_smashed,
203 }),
204 }
205 }
206
207 pub fn gas_coin(&self) -> Option<ObjectID> {
210 self.gas_payment_amount().and_then(|gp| match gp.location {
211 PaymentLocation::Coin(coin_id) => Some(coin_id),
212 PaymentLocation::AddressBalance(_) => None,
213 })
214 }
215
216 pub(crate) fn gas_payment_location(&self) -> Option<PaymentLocation> {
217 match &self.payment {
218 PaymentMetadata::Unmetered | PaymentMetadata::Gasless => None,
219 PaymentMetadata::Smash(metadata) => Some(metadata.gas_charge_location),
220 }
221 }
222
223 pub fn gas_budget(&self) -> u64 {
224 self.gas_status.gas_budget()
225 }
226
227 pub fn unmetered_storage_rebate(&self) -> u64 {
228 self.gas_status.unmetered_storage_rebate()
229 }
230
231 pub fn no_charges(&self) -> bool {
232 self.gas_status.gas_used() == 0
233 && self.gas_status.storage_rebate() == 0
234 && self.gas_status.storage_gas_units() == 0
235 }
236
237 pub fn is_unmetered(&self) -> bool {
238 self.gas_status.is_unmetered()
239 }
240
241 pub fn set_computation_to_budget(&mut self) {
242 self.gas_status.adjust_computation_on_out_of_gas();
243 }
244
245 pub fn move_gas_status(&self) -> &GasStatus {
246 self.gas_status.move_gas_status()
247 }
248
249 pub fn move_gas_status_mut(&mut self) -> &mut GasStatus {
250 self.gas_status.move_gas_status_mut()
251 }
252
253 pub fn into_gas_status(self) -> SuiGasStatus {
254 self.gas_status
255 }
256
257 pub fn summary(&self) -> GasCostSummary {
258 self.gas_status.summary()
259 }
260
261 fn smash_gas(&mut self, temporary_store: &mut TemporaryStore<'_>) {
269 match &mut self.payment {
270 PaymentMetadata::Unmetered | PaymentMetadata::Gasless => (),
271 PaymentMetadata::Smash(smash_metadata) => {
272 smash_metadata.smash_gas(&self.tx_digest, temporary_store);
273 }
274 }
275 }
276
277 pub fn track_storage_mutation(
282 &mut self,
283 object_id: ObjectID,
284 new_size: usize,
285 storage_rebate: u64,
286 ) -> Option<u64> {
287 self.gas_status
288 .track_storage_mutation(object_id, new_size, storage_rebate)
289 }
290
291 pub fn reset_storage_cost_and_rebate(&mut self) {
292 self.gas_status.reset_storage_cost_and_rebate();
293 }
294
295 pub fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
296 self.gas_status.charge_publish_package(size)
297 }
298
299 pub fn charge_input_objects(
301 &mut self,
302 temporary_store: &TemporaryStore<'_>,
303 ) -> Result<(), ExecutionError> {
304 temporary_store
305 .objects()
306 .iter()
307 .filter(|(id, _)| !is_system_package(**id))
309 .try_for_each(|(_, obj)| {
310 let size = obj.object_size_for_gas_metering();
311 if obj.is_package() {
312 self.gas_status.charge_package_object_read(size)
316 } else {
317 self.gas_status.charge_storage_read(size)
318 }
319 })
320 }
321
322 pub fn charge_coin_transfers(
323 &mut self,
324 protocol_config: &ProtocolConfig,
325 num_non_gas_coin_owners: u64,
326 ) -> Result<(), ExecutionError> {
327 let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
331 let cost_per_byte =
334 protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize;
335 let cost_per_owner = bytes_read_per_owner * cost_per_byte;
336 let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize);
337 self.gas_status.charge_storage_read(owner_cost)
338 }
339
340 pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) {
344 temporary_store.drop_writes();
345 self.gas_status.reset_storage_cost_and_rebate();
346 self.smash_gas(temporary_store);
347 temporary_store.ensure_active_inputs_mutated();
348 }
349
350 pub fn round_computation<T, E: ExecutionErrorTrait>(
351 &mut self,
352 result: Result<T, E>,
353 ) -> Result<T, E> {
354 debug_assert!(self.gas_status.storage_rebate() == 0);
355 debug_assert!(self.gas_status.storage_gas_units() == 0);
356
357 if matches!(&self.payment, PaymentMetadata::Unmetered) {
358 return result;
359 }
360 let is_move_abort = matches!(
361 result.as_ref().err().map(|e| e.kind()),
362 Some(sui_types::execution_status::ExecutionErrorKind::MoveAbort(
363 ..
364 ))
365 );
366 let round_res = self.gas_status.bucketize_computation(Some(is_move_abort));
367 match result {
368 Ok(v) => round_res.map(|_| v).map_err(Into::into),
369 Err(e) => Err(e),
370 }
371 }
372
373 pub fn meter_storage(
377 &mut self,
378 temporary_store: &mut TemporaryStore<'_>,
379 ) -> Result<(), ExecutionError> {
380 match &self.payment {
381 PaymentMetadata::Unmetered => {
382 temporary_store.ensure_active_inputs_mutated();
383 temporary_store.collect_storage_and_rebate(self)
384 }
385 PaymentMetadata::Gasless => {
386 temporary_store
387 .check_gasless_execution_requirements()
388 .map_err(|msg| {
389 ExecutionError::new_with_source(
390 sui_types::execution_status::ExecutionErrorKind::InsufficientGas,
391 msg,
392 )
393 })?;
394 temporary_store.ensure_active_inputs_mutated();
395 temporary_store.collect_storage_and_rebate(self)
396 }
397 PaymentMetadata::Smash(_) => {
398 temporary_store.ensure_active_inputs_mutated();
399 temporary_store.collect_storage_and_rebate(self)?;
400 self.gas_status.charge_storage_and_rebate()
401 }
402 }
403 }
404
405 pub(crate) fn handle_error(
406 &mut self,
407 temporary_store: &mut TemporaryStore<'_>,
408 ) -> Result<(), ExecutionError> {
409 self.reset(temporary_store);
410 self.meter_storage(temporary_store).or_else(|_| {
411 self.reset(temporary_store);
413 self.set_computation_to_budget();
414 temporary_store.collect_rebate(self)
415 })
416 }
417
418 pub fn charge<T, E: ExecutionErrorTrait>(
421 &mut self,
422 temporary_store: &mut TemporaryStore<'_>,
423 execution_result: &Result<T, E>,
424 ) -> GasCostSummary {
425 match &self.payment {
426 PaymentMetadata::Unmetered => {
427 let unmetered_storage_rebate = self.gas_status.unmetered_storage_rebate();
429 temporary_store.conserve_unmetered_storage_rebate(unmetered_storage_rebate);
430 GasCostSummary::default()
431 }
432 PaymentMetadata::Gasless => {
433 if execution_result.is_err() {
434 return GasCostSummary::default();
435 }
436 let cost_summary = self.gas_status.summary();
437 let storage_cost = cost_summary.storage_cost;
438 assert!(
439 storage_cost == 0,
440 "Gasless transaction must not incur storage cost, got {storage_cost}"
441 );
442 let sender_rebate = cost_summary.storage_rebate;
443 GasCostSummary {
444 computation_cost: sender_rebate,
445 storage_cost: 0,
446 storage_rebate: sender_rebate,
447 non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
448 }
449 }
450 PaymentMetadata::Smash(metadata) => {
451 if let PaymentLocation::Coin(_) = metadata.gas_charge_location {
452 #[skip_checked_arithmetic]
453 trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
454 }
455 let cost_summary = self.gas_status.summary();
456 self.apply_payment(temporary_store, cost_summary, metadata.gas_charge_location)
457 }
458 }
459 }
460
461 fn apply_payment(
464 &mut self,
465 temporary_store: &mut TemporaryStore<'_>,
466 cost_summary: GasCostSummary,
467 gas_payment_location: PaymentLocation,
468 ) -> GasCostSummary {
469 let net_change = cost_summary.net_gas_usage();
470 match gas_payment_location {
471 PaymentLocation::AddressBalance(payer_address) => {
472 if net_change != 0 {
473 let balance_type = sui_types::balance::Balance::type_tag(
474 sui_types::gas_coin::GAS::type_tag(),
475 );
476 let event = AccumulatorEvent::from_balance_change(
477 payer_address,
478 balance_type,
479 net_change
480 .checked_neg()
481 .expect("net gas usage is never i64::MIN"),
482 )
483 .expect("Failed to create accumulator event for gas charging");
484 temporary_store.add_accumulator_event(event);
485 }
486 }
487 PaymentLocation::Coin(gas_object_id) => {
488 let mut gas_object = temporary_store
489 .read_object(&gas_object_id)
490 .expect("gas coin is an input object and present after smashing")
491 .clone();
492 deduct_gas(&mut gas_object, net_change);
493 #[skip_checked_arithmetic]
494 trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
495 temporary_store.mutate_new_or_input_object(gas_object);
496 }
497 }
498 cost_summary
499 }
500
501 }
503
504 mod legacy {
510 use super::*;
511
512 impl super::GasCharger {
513 pub fn charge_input_objects_legacy(
517 &mut self,
518 temporary_store: &TemporaryStore<'_>,
519 ) -> Result<(), ExecutionError> {
520 let objects = temporary_store.objects();
521 let _object_count = objects.len();
523 let total_size = temporary_store
525 .objects()
526 .iter()
527 .filter(|(id, _)| !is_system_package(**id))
529 .map(|(_, obj)| obj.object_size_for_gas_metering())
530 .sum();
531 self.gas_status.charge_storage_read(total_size)
532 }
533
534 pub(crate) fn legacy_charge_gas<T, E: ExecutionErrorTrait>(
545 &mut self,
546 temporary_store: &mut TemporaryStore<'_>,
547 protocol_config: &ProtocolConfig,
548 execution_result: &mut Result<T, E>,
549 ) -> GasCostSummary {
550 debug_assert!(self.gas_status.storage_rebate() == 0);
553 debug_assert!(self.gas_status.storage_gas_units() == 0);
554
555 if !matches!(&self.payment, PaymentMetadata::Unmetered) {
556 let is_move_abort = execution_result
558 .as_ref()
559 .err()
560 .map(|err| {
561 matches!(
562 err.kind(),
563 sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
564 )
565 })
566 .unwrap_or(false);
567 if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
569 && execution_result.is_ok()
570 {
571 *execution_result = Err(err.into());
572 }
573
574 if execution_result.is_err() {
576 self.reset(temporary_store);
577 }
578 }
579
580 temporary_store.ensure_active_inputs_mutated();
582 temporary_store
583 .collect_storage_and_rebate(self)
584 .expect("storage gas overflow");
585
586 if matches!(&self.payment, PaymentMetadata::Unmetered) {
587 return GasCostSummary::default();
588 }
589 let gas_payment_location = self.gas_payment_location();
590 if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
591 #[skip_checked_arithmetic]
592 trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
593 }
594
595 if execution_result
596 .as_ref()
597 .err()
598 .map(|err| {
599 matches!(
600 err.kind(),
601 sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
602 )
603 })
604 .unwrap_or(false)
605 && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
606 debug_assert!(!protocol_config.early_exit_on_iffw(), "Should have not reached charge gas in this case with IFFW");
607 return GasCostSummary::default();
610 }
611
612 self.compute_storage_and_rebate(temporary_store, execution_result);
613
614 let gas_payment_location = if refresh_gas_payment_location(self.gas_model_version) {
615 self.gas_payment_location()
616 } else {
617 gas_payment_location
618 };
619
620 let cost_summary = self.gas_status.summary();
621
622 let Some(gas_payment_location) = gas_payment_location else {
623 assert!(
625 matches!(self.payment, PaymentMetadata::Gasless),
626 "Only gasless transactions should reach this point without a payment location"
627 );
628 if execution_result.is_err() {
629 return GasCostSummary::default();
630 }
631 let storage_cost = cost_summary.storage_cost;
634 assert!(
635 storage_cost == 0,
636 "Gasless transaction must not incur storage cost, got {storage_cost}"
637 );
638 let sender_rebate = cost_summary.storage_rebate;
639 return GasCostSummary {
640 computation_cost: sender_rebate,
641 storage_cost: 0,
642 storage_rebate: sender_rebate,
643 non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
644 };
645 };
646
647 let net_change = cost_summary.net_gas_usage();
648
649 match gas_payment_location {
650 PaymentLocation::AddressBalance(payer_address) => {
651 if net_change != 0 {
653 let balance_type = sui_types::balance::Balance::type_tag(
654 sui_types::gas_coin::GAS::type_tag(),
655 );
656 let event = AccumulatorEvent::from_balance_change(
657 payer_address,
658 balance_type,
659 net_change.checked_neg().unwrap(),
660 )
661 .expect("Failed to create accumulator event for gas charging");
662 temporary_store.add_accumulator_event(event);
663 }
664 }
665 PaymentLocation::Coin(gas_object_id) => {
666 let mut gas_object =
667 temporary_store.read_object(&gas_object_id).unwrap().clone();
668 deduct_gas(&mut gas_object, net_change);
669 #[skip_checked_arithmetic]
670 trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
671 temporary_store.mutate_new_or_input_object(gas_object);
672 }
673 }
674 cost_summary
675 }
676
677 fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
689 &mut self,
690 temporary_store: &mut TemporaryStore<'_>,
691 execution_result: &mut Result<T, E>,
692 ) {
693 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
694 self.reset(temporary_store);
698 temporary_store.ensure_active_inputs_mutated();
699 temporary_store
700 .collect_storage_and_rebate(self)
701 .expect("storage gas overflow");
702 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
703 self.reset(temporary_store);
706 self.gas_status.adjust_computation_on_out_of_gas();
707 temporary_store.ensure_active_inputs_mutated();
708 temporary_store
709 .collect_rebate(self)
710 .expect("storage gas overflow");
711 if execution_result.is_ok() {
712 *execution_result = Err(err.into());
713 }
714 } else if execution_result.is_ok() {
715 *execution_result = Err(err.into());
716 }
717 }
718 }
719 }
720 }
721
722 impl SmashMetadata {
723 fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
725 std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
726 }
727
728 fn smash_gas(
729 &mut self,
730 tx_digest: &TransactionDigest,
731 temporary_store: &mut TemporaryStore<'_>,
732 ) {
733 self.gas_charge_location = self.smash_target.location();
735
736 let total_smashed = self
738 .payment_methods()
739 .map(|payment| match payment {
740 PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
741 PaymentMethod::Coin(obj_ref) => {
742 let obj_data = temporary_store
743 .objects()
744 .get(&obj_ref.0)
745 .map(|obj| &obj.data);
746 let Some(Data::Move(move_obj)) = obj_data else {
747 return Err(ExecutionError::invariant_violation(
748 "Provided non-gas coin object as input for gas!",
749 ));
750 };
751 if !move_obj.type_().is_gas_coin() {
752 return Err(ExecutionError::invariant_violation(
753 "Provided non-gas coin object as input for gas!",
754 ));
755 }
756 Ok(move_obj.get_coin_value_unsafe())
757 }
758 })
759 .collect::<Result<Vec<u64>, ExecutionError>>()
760 .unwrap_or_else(|_| {
763 panic!(
764 "Unable to process gas payments for transaction {}",
765 tx_digest
766 )
767 })
768 .iter()
769 .sum();
770 debug_assert!(
774 self.total_smashed == 0 || self.total_smashed == total_smashed,
775 "Gas smashing should not change after a reset"
776 );
777 self.total_smashed = total_smashed;
778
779 let smash_location = self.smash_target.location();
780 for payment_method in self.smashed_payments.values() {
782 let location = payment_method.location();
783 assert_ne!(location, smash_location, "Payment methods must be unique");
784 match payment_method {
785 PaymentMethod::AddressBalance(sui_address, reservation) => {
786 assert_reachable!("smashed payment is address-balance reservation");
787 let balance_type = sui_types::balance::Balance::type_tag(
788 sui_types::gas_coin::GAS::type_tag(),
789 );
790 let event = AccumulatorEvent::from_balance_change(
791 *sui_address,
792 balance_type,
793 i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
794 )
795 .expect("Failed to create accumulator event for gas smashing");
796 temporary_store.add_accumulator_event(event);
797 }
798 PaymentMethod::Coin((id, _, _)) => {
799 assert_reachable!("smashed payment is coin object");
800 temporary_store.delete_input_object(id);
801 }
802 }
803 }
804 match &self.smash_target {
805 PaymentMethod::AddressBalance(sui_address, reservation) => {
806 assert_reachable!("smash target is address-balance reservation");
807 let deposit = total_smashed - *reservation;
811 if deposit != 0 {
812 let balance_type = sui_types::balance::Balance::type_tag(
813 sui_types::gas_coin::GAS::type_tag(),
814 );
815 let event = AccumulatorEvent::from_balance_change(
816 *sui_address,
817 balance_type,
818 i64::try_from(deposit).unwrap(),
819 )
820 .expect("Failed to create accumulator event for gas smashing");
821 temporary_store.add_accumulator_event(event);
822 }
823 }
824 PaymentMethod::Coin((gas_coin_id, _, _)) => {
825 let mut primary_gas_object = temporary_store
826 .objects()
827 .get(gas_coin_id)
828 .unwrap_or_else(|| {
830 panic!(
831 "Invariant violation: gas coin not found in store in txn {}",
832 tx_digest
833 )
834 })
835 .clone();
836 primary_gas_object
837 .data
838 .try_as_move_mut()
839 .unwrap_or_else(|| {
841 panic!(
842 "Invariant violation: invalid coin object in txn {}",
843 tx_digest
844 )
845 })
846 .set_coin_value_unsafe(total_smashed);
847 temporary_store.mutate_input_object(primary_gas_object);
848 }
849 }
850 }
851
852 fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
853 self.payment_methods().filter_map(|method| match method {
854 PaymentMethod::Coin(obj_ref) => Some(obj_ref),
855 PaymentMethod::AddressBalance(_, _) => None,
856 })
857 }
858 }
859
860 impl PaymentKind {
861 pub fn unmetered() -> Self {
862 Self(PaymentKind_::Unmetered)
863 }
864
865 pub fn gasless() -> Self {
867 Self(PaymentKind_::Gasless)
868 }
869
870 pub fn smash(payment_methods: Vec<PaymentMethod>) -> Option<Self> {
873 if payment_methods.is_empty() {
874 return None;
875 }
876 let mut unique_methods = IndexMap::new();
877 for payment_method in payment_methods {
878 match (
879 unique_methods.entry(payment_method.location()),
880 payment_method,
881 ) {
882 (indexmap::map::Entry::Vacant(entry), payment_method) => {
883 entry.insert(payment_method);
884 }
885 (
886 indexmap::map::Entry::Occupied(mut occupied),
887 PaymentMethod::AddressBalance(other, additional),
888 ) => {
889 let PaymentMethod::AddressBalance(addr, amount) = occupied.get_mut() else {
890 unreachable!("Payment method does not match location")
891 };
892 assert_eq!(*addr, other, "Payment method does not match location");
893 *amount = amount.checked_add(additional)?;
894 }
895 (indexmap::map::Entry::Occupied(_), _) => return None,
897 }
898 }
899 Some(Self(PaymentKind_::Smash(unique_methods)))
900 }
901 }
902
903 impl PaymentMethod {
904 pub fn location(&self) -> PaymentLocation {
905 match self {
906 PaymentMethod::Coin(obj_ref) => PaymentLocation::Coin(obj_ref.0),
907 PaymentMethod::AddressBalance(addr, _) => PaymentLocation::AddressBalance(*addr),
908 }
909 }
910 }
911}