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 .map(|(_, obj)| obj.object_size_for_gas_metering())
310 .try_for_each(|size| self.gas_status.charge_storage_read(size))
311 }
312
313 pub fn charge_coin_transfers(
314 &mut self,
315 protocol_config: &ProtocolConfig,
316 num_non_gas_coin_owners: u64,
317 ) -> Result<(), ExecutionError> {
318 let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
322 let cost_per_byte =
325 protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize;
326 let cost_per_owner = bytes_read_per_owner * cost_per_byte;
327 let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize);
328 self.gas_status.charge_storage_read(owner_cost)
329 }
330
331 pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) {
335 temporary_store.drop_writes();
336 self.gas_status.reset_storage_cost_and_rebate();
337 self.smash_gas(temporary_store);
338 temporary_store.ensure_active_inputs_mutated();
339 }
340
341 pub fn round_computation<T, E: ExecutionErrorTrait>(
342 &mut self,
343 result: Result<T, E>,
344 ) -> Result<T, E> {
345 debug_assert!(self.gas_status.storage_rebate() == 0);
346 debug_assert!(self.gas_status.storage_gas_units() == 0);
347
348 if matches!(&self.payment, PaymentMetadata::Unmetered) {
349 return result;
350 }
351 let is_move_abort = matches!(
352 result.as_ref().err().map(|e| e.kind()),
353 Some(sui_types::execution_status::ExecutionErrorKind::MoveAbort(
354 ..
355 ))
356 );
357 let round_res = self.gas_status.bucketize_computation(Some(is_move_abort));
358 match result {
359 Ok(v) => round_res.map(|_| v).map_err(Into::into),
360 Err(e) => Err(e),
361 }
362 }
363
364 pub fn meter_storage(
368 &mut self,
369 temporary_store: &mut TemporaryStore<'_>,
370 ) -> Result<(), ExecutionError> {
371 match &self.payment {
372 PaymentMetadata::Unmetered => {
373 temporary_store.ensure_active_inputs_mutated();
374 temporary_store.collect_storage_and_rebate(self)
375 }
376 PaymentMetadata::Gasless => {
377 temporary_store
378 .check_gasless_execution_requirements()
379 .map_err(|msg| {
380 ExecutionError::new_with_source(
381 sui_types::execution_status::ExecutionErrorKind::InsufficientGas,
382 msg,
383 )
384 })?;
385 temporary_store.ensure_active_inputs_mutated();
386 temporary_store.collect_storage_and_rebate(self)
387 }
388 PaymentMetadata::Smash(_) => {
389 temporary_store.ensure_active_inputs_mutated();
390 temporary_store.collect_storage_and_rebate(self)?;
391 self.gas_status.charge_storage_and_rebate()
392 }
393 }
394 }
395
396 pub(crate) fn handle_error(
397 &mut self,
398 temporary_store: &mut TemporaryStore<'_>,
399 ) -> Result<(), ExecutionError> {
400 self.reset(temporary_store);
401 self.meter_storage(temporary_store).or_else(|_| {
402 self.reset(temporary_store);
404 self.set_computation_to_budget();
405 temporary_store.collect_rebate(self)
406 })
407 }
408
409 pub fn charge<T, E: ExecutionErrorTrait>(
412 &mut self,
413 temporary_store: &mut TemporaryStore<'_>,
414 execution_result: &Result<T, E>,
415 ) -> GasCostSummary {
416 match &self.payment {
417 PaymentMetadata::Unmetered => {
418 let unmetered_storage_rebate = self.gas_status.unmetered_storage_rebate();
420 temporary_store.conserve_unmetered_storage_rebate(unmetered_storage_rebate);
421 GasCostSummary::default()
422 }
423 PaymentMetadata::Gasless => {
424 if execution_result.is_err() {
425 return GasCostSummary::default();
426 }
427 let cost_summary = self.gas_status.summary();
428 let storage_cost = cost_summary.storage_cost;
429 assert!(
430 storage_cost == 0,
431 "Gasless transaction must not incur storage cost, got {storage_cost}"
432 );
433 let sender_rebate = cost_summary.storage_rebate;
434 GasCostSummary {
435 computation_cost: sender_rebate,
436 storage_cost: 0,
437 storage_rebate: sender_rebate,
438 non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
439 }
440 }
441 PaymentMetadata::Smash(metadata) => {
442 if let PaymentLocation::Coin(_) = metadata.gas_charge_location {
443 #[skip_checked_arithmetic]
444 trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
445 }
446 let cost_summary = self.gas_status.summary();
447 self.apply_payment(temporary_store, cost_summary, metadata.gas_charge_location)
448 }
449 }
450 }
451
452 fn apply_payment(
455 &mut self,
456 temporary_store: &mut TemporaryStore<'_>,
457 cost_summary: GasCostSummary,
458 gas_payment_location: PaymentLocation,
459 ) -> GasCostSummary {
460 let net_change = cost_summary.net_gas_usage();
461 match gas_payment_location {
462 PaymentLocation::AddressBalance(payer_address) => {
463 if net_change != 0 {
464 let balance_type = sui_types::balance::Balance::type_tag(
465 sui_types::gas_coin::GAS::type_tag(),
466 );
467 let event = AccumulatorEvent::from_balance_change(
468 payer_address,
469 balance_type,
470 net_change
471 .checked_neg()
472 .expect("net gas usage is never i64::MIN"),
473 )
474 .expect("Failed to create accumulator event for gas charging");
475 temporary_store.add_accumulator_event(event);
476 }
477 }
478 PaymentLocation::Coin(gas_object_id) => {
479 let mut gas_object = temporary_store
480 .read_object(&gas_object_id)
481 .expect("gas coin is an input object and present after smashing")
482 .clone();
483 deduct_gas(&mut gas_object, net_change);
484 #[skip_checked_arithmetic]
485 trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
486 temporary_store.mutate_new_or_input_object(gas_object);
487 }
488 }
489 cost_summary
490 }
491
492 }
494
495 mod legacy {
501 use super::*;
502
503 impl super::GasCharger {
504 pub fn charge_input_objects_legacy(
508 &mut self,
509 temporary_store: &TemporaryStore<'_>,
510 ) -> Result<(), ExecutionError> {
511 let objects = temporary_store.objects();
512 let _object_count = objects.len();
514 let total_size = temporary_store
516 .objects()
517 .iter()
518 .filter(|(id, _)| !is_system_package(**id))
520 .map(|(_, obj)| obj.object_size_for_gas_metering())
521 .sum();
522 self.gas_status.charge_storage_read(total_size)
523 }
524
525 pub(crate) fn legacy_charge_gas<T, E: ExecutionErrorTrait>(
536 &mut self,
537 temporary_store: &mut TemporaryStore<'_>,
538 protocol_config: &ProtocolConfig,
539 execution_result: &mut Result<T, E>,
540 ) -> GasCostSummary {
541 debug_assert!(self.gas_status.storage_rebate() == 0);
544 debug_assert!(self.gas_status.storage_gas_units() == 0);
545
546 if !matches!(&self.payment, PaymentMetadata::Unmetered) {
547 let is_move_abort = execution_result
549 .as_ref()
550 .err()
551 .map(|err| {
552 matches!(
553 err.kind(),
554 sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
555 )
556 })
557 .unwrap_or(false);
558 if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
560 && execution_result.is_ok()
561 {
562 *execution_result = Err(err.into());
563 }
564
565 if execution_result.is_err() {
567 self.reset(temporary_store);
568 }
569 }
570
571 temporary_store.ensure_active_inputs_mutated();
573 temporary_store
574 .collect_storage_and_rebate(self)
575 .expect("storage gas overflow");
576
577 if matches!(&self.payment, PaymentMetadata::Unmetered) {
578 return GasCostSummary::default();
579 }
580 let gas_payment_location = self.gas_payment_location();
581 if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
582 #[skip_checked_arithmetic]
583 trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
584 }
585
586 if execution_result
587 .as_ref()
588 .err()
589 .map(|err| {
590 matches!(
591 err.kind(),
592 sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
593 )
594 })
595 .unwrap_or(false)
596 && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
597 debug_assert!(!protocol_config.early_exit_on_iffw(), "Should have not reached charge gas in this case with IFFW");
598 return GasCostSummary::default();
601 }
602
603 self.compute_storage_and_rebate(temporary_store, execution_result);
604
605 let gas_payment_location = if refresh_gas_payment_location(self.gas_model_version) {
606 self.gas_payment_location()
607 } else {
608 gas_payment_location
609 };
610
611 let cost_summary = self.gas_status.summary();
612
613 let Some(gas_payment_location) = gas_payment_location else {
614 assert!(
616 matches!(self.payment, PaymentMetadata::Gasless),
617 "Only gasless transactions should reach this point without a payment location"
618 );
619 if execution_result.is_err() {
620 return GasCostSummary::default();
621 }
622 let storage_cost = cost_summary.storage_cost;
625 assert!(
626 storage_cost == 0,
627 "Gasless transaction must not incur storage cost, got {storage_cost}"
628 );
629 let sender_rebate = cost_summary.storage_rebate;
630 return GasCostSummary {
631 computation_cost: sender_rebate,
632 storage_cost: 0,
633 storage_rebate: sender_rebate,
634 non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
635 };
636 };
637
638 let net_change = cost_summary.net_gas_usage();
639
640 match gas_payment_location {
641 PaymentLocation::AddressBalance(payer_address) => {
642 if net_change != 0 {
644 let balance_type = sui_types::balance::Balance::type_tag(
645 sui_types::gas_coin::GAS::type_tag(),
646 );
647 let event = AccumulatorEvent::from_balance_change(
648 payer_address,
649 balance_type,
650 net_change.checked_neg().unwrap(),
651 )
652 .expect("Failed to create accumulator event for gas charging");
653 temporary_store.add_accumulator_event(event);
654 }
655 }
656 PaymentLocation::Coin(gas_object_id) => {
657 let mut gas_object =
658 temporary_store.read_object(&gas_object_id).unwrap().clone();
659 deduct_gas(&mut gas_object, net_change);
660 #[skip_checked_arithmetic]
661 trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
662 temporary_store.mutate_new_or_input_object(gas_object);
663 }
664 }
665 cost_summary
666 }
667
668 fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
680 &mut self,
681 temporary_store: &mut TemporaryStore<'_>,
682 execution_result: &mut Result<T, E>,
683 ) {
684 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
685 self.reset(temporary_store);
689 temporary_store.ensure_active_inputs_mutated();
690 temporary_store
691 .collect_storage_and_rebate(self)
692 .expect("storage gas overflow");
693 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
694 self.reset(temporary_store);
697 self.gas_status.adjust_computation_on_out_of_gas();
698 temporary_store.ensure_active_inputs_mutated();
699 temporary_store
700 .collect_rebate(self)
701 .expect("storage gas overflow");
702 if execution_result.is_ok() {
703 *execution_result = Err(err.into());
704 }
705 } else if execution_result.is_ok() {
706 *execution_result = Err(err.into());
707 }
708 }
709 }
710 }
711 }
712
713 impl SmashMetadata {
714 fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
716 std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
717 }
718
719 fn smash_gas(
720 &mut self,
721 tx_digest: &TransactionDigest,
722 temporary_store: &mut TemporaryStore<'_>,
723 ) {
724 self.gas_charge_location = self.smash_target.location();
726
727 let total_smashed = self
729 .payment_methods()
730 .map(|payment| match payment {
731 PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
732 PaymentMethod::Coin(obj_ref) => {
733 let obj_data = temporary_store
734 .objects()
735 .get(&obj_ref.0)
736 .map(|obj| &obj.data);
737 let Some(Data::Move(move_obj)) = obj_data else {
738 return Err(ExecutionError::invariant_violation(
739 "Provided non-gas coin object as input for gas!",
740 ));
741 };
742 if !move_obj.type_().is_gas_coin() {
743 return Err(ExecutionError::invariant_violation(
744 "Provided non-gas coin object as input for gas!",
745 ));
746 }
747 Ok(move_obj.get_coin_value_unsafe())
748 }
749 })
750 .collect::<Result<Vec<u64>, ExecutionError>>()
751 .unwrap_or_else(|_| {
754 panic!(
755 "Unable to process gas payments for transaction {}",
756 tx_digest
757 )
758 })
759 .iter()
760 .sum();
761 debug_assert!(
765 self.total_smashed == 0 || self.total_smashed == total_smashed,
766 "Gas smashing should not change after a reset"
767 );
768 self.total_smashed = total_smashed;
769
770 let smash_location = self.smash_target.location();
771 for payment_method in self.smashed_payments.values() {
773 let location = payment_method.location();
774 assert_ne!(location, smash_location, "Payment methods must be unique");
775 match payment_method {
776 PaymentMethod::AddressBalance(sui_address, reservation) => {
777 assert_reachable!("smashed payment is address-balance reservation");
778 let balance_type = sui_types::balance::Balance::type_tag(
779 sui_types::gas_coin::GAS::type_tag(),
780 );
781 let event = AccumulatorEvent::from_balance_change(
782 *sui_address,
783 balance_type,
784 i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
785 )
786 .expect("Failed to create accumulator event for gas smashing");
787 temporary_store.add_accumulator_event(event);
788 }
789 PaymentMethod::Coin((id, _, _)) => {
790 assert_reachable!("smashed payment is coin object");
791 temporary_store.delete_input_object(id);
792 }
793 }
794 }
795 match &self.smash_target {
796 PaymentMethod::AddressBalance(sui_address, reservation) => {
797 assert_reachable!("smash target is address-balance reservation");
798 let deposit = total_smashed - *reservation;
802 if deposit != 0 {
803 let balance_type = sui_types::balance::Balance::type_tag(
804 sui_types::gas_coin::GAS::type_tag(),
805 );
806 let event = AccumulatorEvent::from_balance_change(
807 *sui_address,
808 balance_type,
809 i64::try_from(deposit).unwrap(),
810 )
811 .expect("Failed to create accumulator event for gas smashing");
812 temporary_store.add_accumulator_event(event);
813 }
814 }
815 PaymentMethod::Coin((gas_coin_id, _, _)) => {
816 let mut primary_gas_object = temporary_store
817 .objects()
818 .get(gas_coin_id)
819 .unwrap_or_else(|| {
821 panic!(
822 "Invariant violation: gas coin not found in store in txn {}",
823 tx_digest
824 )
825 })
826 .clone();
827 primary_gas_object
828 .data
829 .try_as_move_mut()
830 .unwrap_or_else(|| {
832 panic!(
833 "Invariant violation: invalid coin object in txn {}",
834 tx_digest
835 )
836 })
837 .set_coin_value_unsafe(total_smashed);
838 temporary_store.mutate_input_object(primary_gas_object);
839 }
840 }
841 }
842
843 fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
844 self.payment_methods().filter_map(|method| match method {
845 PaymentMethod::Coin(obj_ref) => Some(obj_ref),
846 PaymentMethod::AddressBalance(_, _) => None,
847 })
848 }
849 }
850
851 impl PaymentKind {
852 pub fn unmetered() -> Self {
853 Self(PaymentKind_::Unmetered)
854 }
855
856 pub fn gasless() -> Self {
858 Self(PaymentKind_::Gasless)
859 }
860
861 pub fn smash(payment_methods: Vec<PaymentMethod>) -> Option<Self> {
864 if payment_methods.is_empty() {
865 return None;
866 }
867 let mut unique_methods = IndexMap::new();
868 for payment_method in payment_methods {
869 match (
870 unique_methods.entry(payment_method.location()),
871 payment_method,
872 ) {
873 (indexmap::map::Entry::Vacant(entry), payment_method) => {
874 entry.insert(payment_method);
875 }
876 (
877 indexmap::map::Entry::Occupied(mut occupied),
878 PaymentMethod::AddressBalance(other, additional),
879 ) => {
880 let PaymentMethod::AddressBalance(addr, amount) = occupied.get_mut() else {
881 unreachable!("Payment method does not match location")
882 };
883 assert_eq!(*addr, other, "Payment method does not match location");
884 *amount = amount.checked_add(additional)?;
885 }
886 (indexmap::map::Entry::Occupied(_), _) => return None,
888 }
889 }
890 Some(Self(PaymentKind_::Smash(unique_methods)))
891 }
892 }
893
894 impl PaymentMethod {
895 pub fn location(&self) -> PaymentLocation {
896 match self {
897 PaymentMethod::Coin(obj_ref) => PaymentLocation::Coin(obj_ref.0),
898 PaymentMethod::AddressBalance(addr, _) => PaymentLocation::AddressBalance(*addr),
899 }
900 }
901 }
902}