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