1#![cfg_attr(not(feature = "std"), no_std)]
19#![expect(clippy::clone_on_copy, reason = "Comes from Substrate")]
21#![forbid(unsafe_code)]
22#![warn(rust_2018_idioms)]
23
24#[cfg(feature = "runtime-benchmarks")]
25mod benchmarking;
26pub mod migrations;
27#[cfg(test)]
28mod mock;
29#[cfg(test)]
30mod tests;
31pub mod weights;
32
33#[cfg(not(feature = "std"))]
34extern crate alloc;
35
36use domain_runtime_primitives::{MultiAccountId, TryConvertBack};
37use frame_support::dispatch::DispatchResult;
38use frame_support::ensure;
39use frame_support::pallet_prelude::StorageVersion;
40use frame_support::traits::Currency;
41pub use pallet::*;
42use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode};
43use scale_info::TypeInfo;
44use sp_domains::execution_receipt::Transfers;
45use sp_domains::{DomainId, DomainsTransfersTracker};
46use sp_messenger::NoteChainTransfer;
47use sp_messenger::endpoint::EndpointResponse;
48use sp_messenger::messages::ChainId;
49use sp_runtime::traits::{CheckedAdd, CheckedSub, Get};
50use sp_std::vec;
51pub use weights::WeightInfo;
52
53const ZERO_EVM_ADDRESS: MultiAccountId = MultiAccountId::AccountId20([0; 20]);
56const ZERO_SUBSTRATE_ADDRESS: MultiAccountId = MultiAccountId::AccountId32([0; 32]);
57
58#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo, DecodeWithMemTracking)]
60pub struct Location {
61 pub chain_id: ChainId,
63 pub account_id: MultiAccountId,
65}
66
67#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
69pub struct Transfer<Balance> {
70 pub amount: Balance,
72 pub sender: Location,
74 pub receiver: Location,
76}
77
78pub type BalanceOf<T> =
80 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
81
82type MessageIdOf<T> = <<T as Config>::Sender as sp_messenger::endpoint::Sender<
83 <T as frame_system::Config>::AccountId,
84>>::MessageId;
85
86const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
87
88#[frame_support::pallet]
89mod pallet {
90 use crate::weights::WeightInfo;
91 use crate::{
92 BalanceOf, Location, MessageIdOf, MultiAccountId, STORAGE_VERSION, Transfer,
93 TryConvertBack, ZERO_EVM_ADDRESS, ZERO_SUBSTRATE_ADDRESS,
94 };
95 #[cfg(not(feature = "std"))]
96 use alloc::vec::Vec;
97 use frame_support::pallet_prelude::*;
98 use frame_support::traits::{Currency, ExistenceRequirement, WithdrawReasons};
99 use frame_support::weights::Weight;
100 use frame_system::pallet_prelude::*;
101 use parity_scale_codec::{Decode, Encode};
102 use sp_domains::execution_receipt::Transfers;
103 use sp_domains::{DomainId, DomainsTransfersTracker};
104 use sp_messenger::endpoint::{
105 Endpoint, EndpointHandler as EndpointHandlerT, EndpointId, EndpointRequest,
106 EndpointResponse, Sender,
107 };
108 use sp_messenger::messages::ChainId;
109 use sp_runtime::traits::Convert;
110
111 #[pallet::config]
112 pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
113 type SelfChainId: Get<ChainId>;
115
116 type SelfEndpointId: Get<EndpointId>;
118
119 type Currency: Currency<Self::AccountId>;
121
122 type Sender: Sender<Self::AccountId>;
124
125 type AccountIdConverter: TryConvertBack<Self::AccountId, MultiAccountId>;
127
128 type WeightInfo: WeightInfo;
130
131 type MinimumTransfer: Get<BalanceOf<Self>>;
133 }
134
135 #[pallet::pallet]
137 #[pallet::without_storage_info]
138 #[pallet::storage_version(STORAGE_VERSION)]
139 pub struct Pallet<T>(_);
140
141 #[pallet::storage]
143 #[pallet::getter(fn outgoing_transfers)]
144 pub(super) type OutgoingTransfers<T: Config> = StorageDoubleMap<
145 _,
146 Identity,
147 ChainId,
148 Identity,
149 MessageIdOf<T>,
150 Transfer<BalanceOf<T>>,
151 OptionQuery,
152 >;
153
154 #[pallet::storage]
156 #[pallet::getter(fn domain_balances)]
157 pub(super) type DomainBalances<T: Config> =
158 StorageMap<_, Identity, DomainId, BalanceOf<T>, ValueQuery>;
159
160 #[pallet::storage]
165 #[pallet::getter(fn all_domains_supply)]
166 pub(super) type AllDomainsSupply<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
167
168 #[pallet::storage]
171 #[pallet::getter(fn chain_transfers)]
172 pub(super) type ChainTransfers<T: Config> =
173 StorageValue<_, Transfers<BalanceOf<T>>, ValueQuery>;
174
175 #[pallet::storage]
177 #[pallet::getter(fn unconfirmed_transfers)]
178 pub(super) type UnconfirmedTransfers<T: Config> =
179 StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
180
181 #[pallet::storage]
183 #[pallet::getter(fn cancelled_transfers)]
184 pub(super) type CancelledTransfers<T: Config> =
185 StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
186
187 #[pallet::event]
189 #[pallet::generate_deposit(pub (super) fn deposit_event)]
190 pub enum Event<T: Config> {
191 OutgoingTransferInitiated {
193 chain_id: ChainId,
195 message_id: MessageIdOf<T>,
197 amount: BalanceOf<T>,
199 },
200
201 OutgoingTransferFailed {
203 chain_id: ChainId,
205 message_id: MessageIdOf<T>,
207 err: DispatchError,
209 },
210
211 OutgoingTransferSuccessful {
213 chain_id: ChainId,
215 message_id: MessageIdOf<T>,
217 },
218
219 IncomingTransferSuccessful {
221 chain_id: ChainId,
223 message_id: MessageIdOf<T>,
225 amount: BalanceOf<T>,
227 },
228 }
229
230 #[pallet::error]
232 pub enum Error<T> {
233 LowBalance,
235 InvalidPayload,
237 MissingTransferRequest,
239 InvalidTransferRequest,
241 UnexpectedMessage,
243 InvalidAccountId,
245 LowBalanceOnDomain,
247 NonConsensusChain,
249 BalanceOverflow,
251 BalanceUnderflow,
253 DomainBalanceAlreadyInitialized,
255 MinimumTransferAmount,
257 }
258
259 #[pallet::call]
260 impl<T: Config> Pallet<T> {
261 #[pallet::call_index(0)]
264 #[pallet::weight(T::WeightInfo::transfer())]
265 pub fn transfer(
266 origin: OriginFor<T>,
267 dst_location: Location,
268 amount: BalanceOf<T>,
269 ) -> DispatchResult {
270 let sender = ensure_signed(origin)?;
271 ensure!(
272 amount >= T::MinimumTransfer::get(),
273 Error::<T>::MinimumTransferAmount
274 );
275
276 ensure!(
277 dst_location.account_id != ZERO_EVM_ADDRESS,
278 Error::<T>::InvalidAccountId
279 );
280
281 ensure!(
282 dst_location.account_id != ZERO_SUBSTRATE_ADDRESS,
283 Error::<T>::InvalidAccountId
284 );
285
286 let _imbalance = T::Currency::withdraw(
288 &sender,
289 amount,
290 WithdrawReasons::TRANSFER,
291 ExistenceRequirement::KeepAlive,
292 )
293 .map_err(|_| Error::<T>::LowBalance)?;
294
295 let dst_chain_id = dst_location.chain_id;
297 let transfer = Transfer {
298 amount,
299 sender: Location {
300 chain_id: T::SelfChainId::get(),
301 account_id: T::AccountIdConverter::convert(sender.clone()),
302 },
303 receiver: dst_location,
304 };
305
306 let message_id = T::Sender::send_message(
308 &sender,
309 dst_chain_id,
310 EndpointRequest {
311 src_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
312 dst_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
314 payload: transfer.encode(),
315 },
316 )?;
317
318 OutgoingTransfers::<T>::insert(dst_chain_id, message_id, transfer);
319 Self::deposit_event(Event::<T>::OutgoingTransferInitiated {
320 chain_id: dst_chain_id,
321 message_id,
322 amount,
323 });
324
325 if T::SelfChainId::get().is_consensus_chain() {
328 Self::note_transfer(T::SelfChainId::get(), dst_chain_id, amount)?
329 } else {
330 ChainTransfers::<T>::try_mutate(|transfers| {
331 Self::update_transfer_out(transfers, dst_chain_id, amount)
332 })?;
333 }
334
335 Ok(())
336 }
337 }
338
339 #[pallet::hooks]
340 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
341 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
342 ChainTransfers::<T>::set(Default::default());
346 T::DbWeight::get().writes(1)
347 }
348 }
349
350 impl<T: Config> Pallet<T> {
351 pub fn transfers_storage_key() -> Vec<u8> {
352 use frame_support::storage::generator::StorageValue;
353 ChainTransfers::<T>::storage_value_final_key().to_vec()
354 }
355
356 pub(super) fn increase_all_domains_supply(amount: BalanceOf<T>) {
357 let current = Self::all_domains_supply();
358 let updated = current.checked_add(&amount).unwrap_or_else(|| {
359 log::error!(
360 target: "runtime::transporter",
361 "all-domains supply overflow adding {amount:?}; keeping current (drift)"
362 );
363 current
364 });
365 AllDomainsSupply::<T>::put(updated);
366 }
367
368 pub(super) fn decrease_all_domains_supply(amount: BalanceOf<T>) {
369 let current = Self::all_domains_supply();
370 let updated = current.checked_sub(&amount).unwrap_or_else(|| {
371 log::error!(
372 target: "runtime::transporter",
373 "all-domains supply underflow subtracting {amount:?}; flooring at zero (drift)"
374 );
375 BalanceOf::<T>::default()
376 });
377 AllDomainsSupply::<T>::put(updated);
378 }
379 }
380
381 #[derive(Debug)]
383 pub struct EndpointHandler<T>(pub PhantomData<T>);
384
385 impl<T: Config> EndpointHandlerT<MessageIdOf<T>> for EndpointHandler<T> {
386 fn message(
387 &self,
388 src_chain_id: ChainId,
389 message_id: MessageIdOf<T>,
390 req: EndpointRequest,
391 pre_check_result: DispatchResult,
392 ) -> EndpointResponse {
393 let dst_endpoint = req.dst_endpoint;
395 let req = match Transfer::decode(&mut req.payload.as_slice()) {
396 Ok(req) => req,
397 Err(_) => return Err(Error::<T>::InvalidPayload.into()),
398 };
399
400 let pre_check_handler = || {
401 ensure!(
403 T::SelfChainId::get() != src_chain_id,
404 Error::<T>::InvalidTransferRequest
405 );
406
407 ensure!(
409 dst_endpoint == Endpoint::Id(T::SelfEndpointId::get()),
410 Error::<T>::UnexpectedMessage
411 );
412
413 pre_check_result
414 };
415
416 let amount = req.amount;
417 let response = match pre_check_handler() {
418 Ok(_) => Pallet::<T>::finalize_transfer(src_chain_id, message_id, req),
419 Err(err) => Err(err),
420 };
421
422 if response.is_err() {
423 if T::SelfChainId::get().is_consensus_chain() {
426 Pallet::<T>::reject_transfer(src_chain_id, T::SelfChainId::get(), amount)?;
427 } else {
428 ChainTransfers::<T>::try_mutate(|transfers| {
429 Pallet::<T>::update_transfer_rejected(transfers, src_chain_id, amount)
430 })?;
431 }
432 }
433
434 response
435 }
436
437 fn message_weight(&self) -> Weight {
438 T::WeightInfo::message()
439 }
440
441 fn message_response(
442 &self,
443 dst_chain_id: ChainId,
444 message_id: MessageIdOf<T>,
445 req: EndpointRequest,
446 resp: EndpointResponse,
447 ) -> DispatchResult {
448 let transfer = OutgoingTransfers::<T>::take(dst_chain_id, message_id)
450 .ok_or(Error::<T>::MissingTransferRequest)?;
451 ensure!(
452 req.payload == transfer.encode(),
453 Error::<T>::InvalidTransferRequest
454 );
455
456 match resp {
458 Ok(_) => {
459 frame_system::Pallet::<T>::deposit_event(Into::<
461 <T as frame_system::Config>::RuntimeEvent,
462 >::into(
463 Event::<T>::OutgoingTransferSuccessful {
464 chain_id: dst_chain_id,
465 message_id,
466 },
467 ));
468 }
469 Err(err) => {
470 let account_id =
473 T::AccountIdConverter::try_convert_back(transfer.sender.account_id)
474 .ok_or(Error::<T>::InvalidAccountId)?;
475
476 if T::SelfChainId::get().is_consensus_chain() {
479 Pallet::<T>::claim_rejected_transfer(
480 T::SelfChainId::get(),
481 dst_chain_id,
482 transfer.amount,
483 )?;
484 } else {
485 ChainTransfers::<T>::try_mutate(|transfers| {
486 Pallet::<T>::update_transfer_revert(
487 transfers,
488 dst_chain_id,
489 transfer.amount,
490 )
491 })?;
492 }
493
494 let _imbalance = T::Currency::deposit_creating(&account_id, transfer.amount);
495 frame_system::Pallet::<T>::deposit_event(Into::<
496 <T as frame_system::Config>::RuntimeEvent,
497 >::into(
498 Event::<T>::OutgoingTransferFailed {
499 chain_id: dst_chain_id,
500 message_id,
501 err,
502 },
503 ));
504 }
505 }
506
507 Ok(())
508 }
509
510 fn message_response_weight(&self) -> Weight {
511 T::WeightInfo::message_response()
512 }
513 }
514}
515
516impl<T: Config> sp_domains::DomainsTransfersTracker<BalanceOf<T>> for Pallet<T> {
517 type Error = Error<T>;
518
519 fn initialize_domain_balance(
520 domain_id: DomainId,
521 amount: BalanceOf<T>,
522 ) -> Result<(), Self::Error> {
523 Self::ensure_consensus_chain()?;
524
525 ensure!(
526 !DomainBalances::<T>::contains_key(domain_id),
527 Error::DomainBalanceAlreadyInitialized
528 );
529
530 DomainBalances::<T>::set(domain_id, amount);
531 Self::increase_all_domains_supply(amount);
532 Ok(())
533 }
534
535 fn note_transfer(
536 from_chain_id: ChainId,
537 to_chain_id: ChainId,
538 amount: BalanceOf<T>,
539 ) -> Result<(), Self::Error> {
540 Self::ensure_consensus_chain()?;
541
542 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
543 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
544 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
545 *current_balance = current_balance
546 .checked_sub(&amount)
547 .ok_or(Error::LowBalanceOnDomain)?;
548 Ok(())
549 })?;
550 }
551
552 *total_amount = total_amount
553 .checked_add(&amount)
554 .ok_or(Error::BalanceOverflow)?;
555 Ok(())
556 })?;
557
558 if from_chain_id.maybe_domain_chain().is_none() {
560 Self::increase_all_domains_supply(amount);
561 }
562
563 Ok(())
564 }
565
566 fn confirm_transfer(
567 from_chain_id: ChainId,
568 to_chain_id: ChainId,
569 amount: BalanceOf<T>,
570 ) -> Result<(), Self::Error> {
571 Self::ensure_consensus_chain()?;
572 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
573 *total_amount = total_amount
574 .checked_sub(&amount)
575 .ok_or(Error::BalanceUnderflow)?;
576
577 if let Some(domain_id) = to_chain_id.maybe_domain_chain() {
578 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
579 *current_balance = current_balance
580 .checked_add(&amount)
581 .ok_or(Error::BalanceOverflow)?;
582 Ok(())
583 })?;
584 }
585
586 Ok(())
587 })?;
588
589 if to_chain_id.maybe_domain_chain().is_none() {
591 Self::decrease_all_domains_supply(amount);
592 }
593
594 Ok(())
595 }
596
597 fn claim_rejected_transfer(
598 from_chain_id: ChainId,
599 to_chain_id: ChainId,
600 amount: BalanceOf<T>,
601 ) -> Result<(), Self::Error> {
602 Self::ensure_consensus_chain()?;
603 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
604 *total_amount = total_amount
605 .checked_sub(&amount)
606 .ok_or(Error::BalanceUnderflow)?;
607
608 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
609 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
610 *current_balance = current_balance
611 .checked_add(&amount)
612 .ok_or(Error::BalanceOverflow)?;
613 Ok(())
614 })?;
615 }
616
617 Ok(())
618 })?;
619
620 if from_chain_id.maybe_domain_chain().is_none() {
622 Self::decrease_all_domains_supply(amount);
623 }
624
625 Ok(())
626 }
627
628 fn reject_transfer(
629 from_chain_id: ChainId,
630 to_chain_id: ChainId,
631 amount: BalanceOf<T>,
632 ) -> Result<(), Self::Error> {
633 Self::ensure_consensus_chain()?;
634 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
637 *total_amount = total_amount
638 .checked_sub(&amount)
639 .ok_or(Error::BalanceUnderflow)?;
640
641 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
642 *total_amount = total_amount
643 .checked_add(&amount)
644 .ok_or(Error::BalanceOverflow)?;
645 Ok(())
646 })?;
647
648 Ok(())
649 })?;
650
651 Ok(())
652 }
653
654 fn reduce_domain_balance(domain_id: DomainId, amount: BalanceOf<T>) -> Result<(), Self::Error> {
655 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
656 *current_balance = current_balance
657 .checked_sub(&amount)
658 .ok_or(Error::LowBalanceOnDomain)?;
659 Ok(())
660 })?;
661 Self::decrease_all_domains_supply(amount);
662 Ok(())
663 }
664}
665
666impl<T: Config> NoteChainTransfer<BalanceOf<T>> for Pallet<T> {
667 fn note_transfer_in(amount: BalanceOf<T>, from_chain_id: ChainId) -> bool {
668 let result: DispatchResult = if T::SelfChainId::get().is_consensus_chain() {
669 Pallet::<T>::confirm_transfer(from_chain_id, T::SelfChainId::get(), amount)
670 .map_err(Into::into)
671 } else {
672 ChainTransfers::<T>::try_mutate(|transfers| {
673 Pallet::<T>::update_transfer_in(transfers, from_chain_id, amount)
674 })
675 };
676 if let Err(err) = result {
677 log::error!(
678 target: "runtime::transporter",
679 "note_transfer_in from {from_chain_id:?} failed: {err:?}"
680 );
681 return false;
682 }
683 true
684 }
685
686 fn note_transfer_out(amount: BalanceOf<T>, to_chain_id: ChainId) -> bool {
687 let result: DispatchResult = if T::SelfChainId::get().is_consensus_chain() {
688 Self::note_transfer(T::SelfChainId::get(), to_chain_id, amount).map_err(Into::into)
689 } else {
690 ChainTransfers::<T>::try_mutate(|transfers| {
691 Self::update_transfer_out(transfers, to_chain_id, amount)
692 })
693 };
694 if let Err(err) = result {
695 log::error!(
696 target: "runtime::transporter",
697 "note_transfer_out to {to_chain_id:?} failed: {err:?}"
698 );
699 return false;
700 }
701 true
702 }
703}
704
705impl<T: Config> Pallet<T> {
706 fn ensure_consensus_chain() -> Result<(), Error<T>> {
707 ensure!(
708 T::SelfChainId::get().is_consensus_chain(),
709 Error::NonConsensusChain
710 );
711
712 Ok(())
713 }
714
715 fn finalize_transfer(
716 src_chain_id: ChainId,
717 message_id: MessageIdOf<T>,
718 req: Transfer<BalanceOf<T>>,
719 ) -> EndpointResponse {
720 let account_id = T::AccountIdConverter::try_convert_back(req.receiver.account_id)
722 .ok_or(Error::<T>::InvalidAccountId)?;
723
724 if T::SelfChainId::get().is_consensus_chain() {
727 Pallet::<T>::confirm_transfer(src_chain_id, T::SelfChainId::get(), req.amount)?
728 } else {
729 ChainTransfers::<T>::try_mutate(|transfers| {
730 Pallet::<T>::update_transfer_in(transfers, src_chain_id, req.amount)
731 })?;
732 }
733
734 let _imbalance = T::Currency::deposit_creating(&account_id, req.amount);
735
736 frame_system::Pallet::<T>::deposit_event(
737 Into::<<T as frame_system::Config>::RuntimeEvent>::into(
738 Event::<T>::IncomingTransferSuccessful {
739 chain_id: src_chain_id,
740 message_id,
741 amount: req.amount,
742 },
743 ),
744 );
745 Ok(vec![])
746 }
747
748 fn update_transfer_out(
749 transfers: &mut Transfers<BalanceOf<T>>,
750 to_chain_id: ChainId,
751 amount: BalanceOf<T>,
752 ) -> DispatchResult {
753 let total_transfer =
754 if let Some(current_transfer_amount) = transfers.transfers_out.get(&to_chain_id) {
755 current_transfer_amount
756 .checked_add(&amount)
757 .ok_or(Error::<T>::BalanceOverflow)?
758 } else {
759 amount
760 };
761 transfers.transfers_out.insert(to_chain_id, total_transfer);
762 Ok(())
763 }
764
765 fn update_transfer_in(
766 transfers: &mut Transfers<BalanceOf<T>>,
767 from_chain_id: ChainId,
768 amount: BalanceOf<T>,
769 ) -> DispatchResult {
770 let total_transfer =
771 if let Some(current_transfer_amount) = transfers.transfers_in.get(&from_chain_id) {
772 current_transfer_amount
773 .checked_add(&amount)
774 .ok_or(Error::<T>::BalanceOverflow)?
775 } else {
776 amount
777 };
778 transfers.transfers_in.insert(from_chain_id, total_transfer);
779 Ok(())
780 }
781
782 fn update_transfer_revert(
783 transfers: &mut Transfers<BalanceOf<T>>,
784 to_chain_id: ChainId,
785 amount: BalanceOf<T>,
786 ) -> DispatchResult {
787 let total_transfer = if let Some(current_transfer_amount) =
788 transfers.rejected_transfers_claimed.get(&to_chain_id)
789 {
790 current_transfer_amount
791 .checked_add(&amount)
792 .ok_or(Error::<T>::BalanceOverflow)?
793 } else {
794 amount
795 };
796 transfers
797 .rejected_transfers_claimed
798 .insert(to_chain_id, total_transfer);
799 Ok(())
800 }
801
802 fn update_transfer_rejected(
803 transfers: &mut Transfers<BalanceOf<T>>,
804 from_chain_id: ChainId,
805 amount: BalanceOf<T>,
806 ) -> DispatchResult {
807 let total_transfer = if let Some(current_transfer_amount) =
808 transfers.transfers_rejected.get(&from_chain_id)
809 {
810 current_transfer_amount
811 .checked_add(&amount)
812 .ok_or(Error::<T>::BalanceOverflow)?
813 } else {
814 amount
815 };
816 transfers
817 .transfers_rejected
818 .insert(from_chain_id, total_transfer);
819 Ok(())
820 }
821}