1#![cfg_attr(not(feature = "std"), no_std)]
21
22extern crate alloc;
23
24use alloc::{boxed::Box, vec, vec::Vec};
25use codec::Encode;
26use core::marker::PhantomData;
27use cumulus_primitives_core::{MessageSendError, UpwardMessageSender};
28use frame_support::{
29 defensive,
30 traits::{
31 tokens::{fungibles, imbalance::UnsafeManualAccounting},
32 Get, OnUnbalanced as OnUnbalancedT,
33 },
34 weights::{Weight, WeightToFee as WeightToFeeT},
35};
36use pallet_asset_conversion::{QuotePrice, SwapCredit as SwapCreditT};
37use polkadot_runtime_common::xcm_sender::PriceForMessageDelivery;
38use sp_runtime::traits::Zero;
39use xcm::{latest::prelude::*, VersionedLocation, VersionedXcm, WrapVersion};
40use xcm_builder::InspectMessageQueues;
41use xcm_executor::{
42 traits::{MatchesFungibles, WeightTrader},
43 AssetsInHolding,
44};
45
46#[cfg(test)]
47mod tests;
48
49#[cfg(test)]
50mod test_helpers {
51 pub use xcm_executor::test_helpers::mock_asset_to_holding as asset_to_holding;
52}
53
54pub struct ParentAsUmp<T, W, P>(PhantomData<(T, W, P)>);
62
63impl<T, W, P> SendXcm for ParentAsUmp<T, W, P>
64where
65 T: UpwardMessageSender,
66 W: WrapVersion,
67 P: PriceForMessageDelivery<Id = ()>,
68{
69 type Ticket = Vec<u8>;
70
71 fn validate(dest: &mut Option<Location>, msg: &mut Option<Xcm<()>>) -> SendResult<Vec<u8>> {
72 let d = dest.take().ok_or(SendError::MissingArgument)?;
73
74 if d.contains_parents_only(1) {
75 let xcm = msg.take().ok_or(SendError::MissingArgument)?;
77 let price = P::price_for_delivery((), &xcm);
78 let versioned_xcm =
79 W::wrap_version(&d, xcm).map_err(|()| SendError::DestinationUnsupported)?;
80 versioned_xcm
81 .check_is_decodable()
82 .map_err(|()| SendError::ExceedsMaxMessageSize)?;
83 let data = versioned_xcm.encode();
84
85 T::can_send_upward_message(&data).map_err(Self::map_upward_sender_err)?;
87
88 Ok((data, price))
89 } else {
90 *dest = Some(d);
93 Err(SendError::NotApplicable)
94 }
95 }
96
97 fn deliver(data: Vec<u8>) -> Result<XcmHash, SendError> {
98 let (_, hash) = T::send_upward_message(data).map_err(Self::map_upward_sender_err)?;
99 Ok(hash)
100 }
101
102 #[cfg(feature = "runtime-benchmarks")]
103 fn ensure_successful_delivery(location: Option<Location>) {
104 if location.as_ref().map_or(false, |l| l.contains_parents_only(1)) {
105 T::ensure_successful_delivery();
106 }
107 }
108}
109
110impl<T, W, P> ParentAsUmp<T, W, P> {
111 fn map_upward_sender_err(message_send_error: MessageSendError) -> SendError {
112 match message_send_error {
113 MessageSendError::TooBig => SendError::ExceedsMaxMessageSize,
114 e => SendError::Transport(e.into()),
115 }
116 }
117}
118
119impl<T: UpwardMessageSender + InspectMessageQueues, W, P> InspectMessageQueues
120 for ParentAsUmp<T, W, P>
121{
122 fn clear_messages() {
123 T::clear_messages();
124 }
125
126 fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
127 T::get_messages()
128 }
129}
130
131pub struct TakeFirstAssetTrader<
140 AccountId: Eq,
141 FeeCharger: ChargeWeightInFungibles<AccountId, Fungibles>,
142 Matcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
143 Fungibles: fungibles::Balanced<AccountId>,
144 OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
145> {
146 outstanding_credit: Option<fungibles::Credit<AccountId, Fungibles>>,
148 weight_outstanding: Weight,
150 _phantom_data: PhantomData<(AccountId, FeeCharger, Matcher, Fungibles, OnUnbalanced)>,
151}
152
153impl<
154 AccountId: Eq,
155 FeeCharger: ChargeWeightInFungibles<AccountId, Fungibles>,
156 Matcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
157 Fungibles: fungibles::Inspect<AccountId, Balance = u128, AssetId: Into<Location> + 'static>
158 + fungibles::Balanced<AccountId, OnDropCredit: 'static, OnDropDebt: 'static>,
159 OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
160 > WeightTrader for TakeFirstAssetTrader<AccountId, FeeCharger, Matcher, Fungibles, OnUnbalanced>
161{
162 fn new() -> Self {
163 Self {
164 outstanding_credit: None,
165 weight_outstanding: Weight::zero(),
166 _phantom_data: PhantomData,
167 }
168 }
169 fn buy_weight(
173 &mut self,
174 weight: Weight,
175 mut payment: AssetsInHolding,
176 context: &XcmContext,
177 ) -> Result<AssetsInHolding, (AssetsInHolding, XcmError)> {
178 log::trace!(target: "xcm::weight", "TakeFirstAssetTrader::buy_weight weight: {:?}, payment: {:?}, context: {:?}", weight, payment, context);
179
180 if self.outstanding_credit.is_some() {
182 return Err((payment, XcmError::NotWithdrawable));
183 }
184
185 let Some(used) = payment.fungible_assets_iter().next() else {
187 return Err((payment, XcmError::AssetNotFound));
188 };
189
190 let Ok((fungibles_asset_id, _)) = Matcher::matches_fungibles(&used) else {
192 return Err((payment, XcmError::AssetNotFound));
193 };
194
195 let required_amount: u128 =
199 match FeeCharger::charge_weight_in_fungibles(fungibles_asset_id.clone(), weight)
200 .map(|amount| amount.max(Fungibles::minimum_balance(fungibles_asset_id.clone())))
201 {
202 Ok(a) => a,
203 Err(_) => return Err((payment, XcmError::Overflow)),
204 };
205
206 let required = used.id.into_asset(required_amount.into());
208
209 let Some(imbalance) = payment
213 .try_take(required.into())
214 .ok()
215 .and_then(|taken| taken.fungible.into_iter().next().map(|(_, v)| v))
216 else {
217 return Err((payment, XcmError::TooExpensive));
218 };
219 let mut credit = fungibles::Credit::<AccountId, Fungibles>::zero(fungibles_asset_id);
221 credit.saturating_subsume(imbalance);
222
223 self.outstanding_credit = Some(credit);
225 self.weight_outstanding = weight;
226
227 Ok(payment)
229 }
230
231 fn refund_weight(&mut self, weight: Weight, context: &XcmContext) -> Option<AssetsInHolding> {
239 log::trace!(target: "xcm::weight", "TakeFirstAssetTrader::refund_weight weight: {:?}, context: {:?}", weight, context);
240 if weight.is_zero() {
241 return None;
242 }
243 let outstanding_credit = self.outstanding_credit.as_mut()?;
244 let id = outstanding_credit.asset();
245 let fun = Fungible(outstanding_credit.peek());
246 let asset = (id.clone(), fun).into();
247
248 let (fungibles_asset_id, _) = Matcher::matches_fungibles(&asset).ok()?;
250 let minimum_balance = Fungibles::minimum_balance(fungibles_asset_id.clone());
251
252 let refund_credit = FeeCharger::charge_weight_in_fungibles(fungibles_asset_id, weight)
255 .ok()
256 .map(|refund_balance| {
257 if outstanding_credit.peek().saturating_sub(refund_balance) >= minimum_balance {
260 outstanding_credit.extract(refund_balance)
261 } else {
262 let keep = minimum_balance.min(outstanding_credit.peek());
265 let refund_amount = outstanding_credit.peek().saturating_sub(keep);
266 outstanding_credit.extract(refund_amount)
267 }
268 })?;
269 self.weight_outstanding = self.weight_outstanding.saturating_sub(weight);
271
272 if refund_credit.peek() != Zero::zero() {
274 Some(AssetsInHolding::new_from_fungible_credit(asset.id, Box::new(refund_credit)))
275 } else {
276 None
277 }
278 }
279
280 fn quote_weight(
281 &mut self,
282 weight: Weight,
283 given_id: AssetId,
284 context: &XcmContext,
285 ) -> Result<Asset, XcmError> {
286 log::trace!(
287 target: "xcm::weight",
288 "TakeFirstAssetTrader::quote_weight weight: {:?}, given_id: {:?}, context: {:?}",
289 weight, given_id, context
290 );
291 if weight.is_zero() {
292 return Err(XcmError::NoDeal);
293 }
294
295 let give_matcher: Asset = (given_id.clone(), 1).into();
296 let (give_fungibles_id, _) =
298 Matcher::matches_fungibles(&give_matcher).map_err(|_| XcmError::AssetNotFound)?;
299
300 let required_amount: u128 =
304 FeeCharger::charge_weight_in_fungibles(give_fungibles_id.clone(), weight)
305 .map(|amount| amount.max(Fungibles::minimum_balance(give_fungibles_id.clone())))
306 .map_err(|_| XcmError::Overflow)?;
307
308 let required = given_id.into_asset(required_amount.into());
310 Ok(required)
311 }
312}
313
314impl<
315 AccountId: Eq,
316 FeeCharger: ChargeWeightInFungibles<AccountId, Fungibles>,
317 Matcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
318 Fungibles: fungibles::Balanced<AccountId>,
319 OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
320 > Drop for TakeFirstAssetTrader<AccountId, FeeCharger, Matcher, Fungibles, OnUnbalanced>
321{
322 fn drop(&mut self) {
323 self.outstanding_credit
324 .take()
325 .filter(|credit| !credit.peek().is_zero())
326 .map(OnUnbalanced::on_unbalanced);
327 }
328}
329
330pub trait ChargeWeightInFungibles<AccountId, Assets: fungibles::Inspect<AccountId>> {
334 fn charge_weight_in_fungibles(
335 asset_id: <Assets as fungibles::Inspect<AccountId>>::AssetId,
336 weight: Weight,
337 ) -> Result<<Assets as fungibles::Inspect<AccountId>>::Balance, XcmError>;
338}
339
340pub struct SwapFirstAssetTrader<
356 Target: Get<Fungibles::AssetId>,
357 SwapCredit: SwapCreditT<
358 AccountId,
359 Balance = Fungibles::Balance,
360 AssetKind = Fungibles::AssetId,
361 Credit = fungibles::Credit<AccountId, Fungibles>,
362 > + QuotePrice,
363 WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
364 Fungibles: fungibles::Balanced<AccountId>,
365 FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
366 OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
367 AccountId,
368> where
369 Fungibles::Balance: From<u128> + Into<u128>,
370{
371 total_fee: fungibles::Credit<AccountId, Fungibles>,
373 last_fee_asset: Option<AssetId>,
375 _phantom_data: PhantomData<(
376 Target,
377 SwapCredit,
378 WeightToFee,
379 Fungibles,
380 FungiblesAssetMatcher,
381 OnUnbalanced,
382 AccountId,
383 )>,
384}
385
386impl<
387 Target: Get<Fungibles::AssetId>,
388 SwapCredit: SwapCreditT<
389 AccountId,
390 Balance = Fungibles::Balance,
391 AssetKind = Fungibles::AssetId,
392 Credit = fungibles::Credit<AccountId, Fungibles>,
393 > + QuotePrice<AssetKind = Fungibles::AssetId, Balance = Fungibles::Balance>,
394 WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
395 Fungibles: fungibles::Balanced<
396 AccountId,
397 AssetId: 'static,
398 OnDropCredit: 'static,
399 OnDropDebt: 'static,
400 >,
401 FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
402 OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
403 AccountId,
404 > WeightTrader
405 for SwapFirstAssetTrader<
406 Target,
407 SwapCredit,
408 WeightToFee,
409 Fungibles,
410 FungiblesAssetMatcher,
411 OnUnbalanced,
412 AccountId,
413 >
414where
415 Fungibles::Balance: From<u128> + Into<u128>,
416{
417 fn new() -> Self {
418 Self {
419 total_fee: fungibles::Credit::<AccountId, Fungibles>::zero(Target::get()),
420 last_fee_asset: None,
421 _phantom_data: PhantomData,
422 }
423 }
424
425 fn buy_weight(
426 &mut self,
427 weight: Weight,
428 mut payment: AssetsInHolding,
429 _context: &XcmContext,
430 ) -> Result<AssetsInHolding, (AssetsInHolding, XcmError)> {
431 log::trace!(
432 target: "xcm::weight",
433 "SwapFirstAssetTrader::buy_weight weight: {:?}, payment: {:?}",
434 weight,
435 payment,
436 );
437 let Some((id, given_credit)) = payment.fungible.first_key_value() else {
438 return Err((payment, XcmError::AssetNotFound));
439 };
440 let id = id.clone();
441 let given_credit_amount = given_credit.amount();
442 let first_asset: Asset = (id.clone(), given_credit_amount).into();
443 let Ok((fungibles_id, _)) = FungiblesAssetMatcher::matches_fungibles(&first_asset) else {
444 log::trace!(
445 target: "xcm::weight",
446 "SwapFirstAssetTrader::buy_weight asset {:?} didn't match",
447 first_asset,
448 );
449 return Err((payment, XcmError::AssetNotFound));
450 };
451
452 let swap_asset = fungibles_id.clone().into();
453 if Target::get().eq(&swap_asset) {
454 log::trace!(
455 target: "xcm::weight",
456 "SwapFirstAssetTrader::buy_weight Asset was same as Target, swap not needed.",
457 );
458 return Err((payment, XcmError::FeesNotMet));
460 }
461 let Some(imbalance) = payment.fungible.remove(&first_asset.id) else {
463 return Err((payment, XcmError::TooExpensive));
464 };
465 let mut credit_in = fungibles::Credit::<AccountId, Fungibles>::zero(fungibles_id);
467 credit_in.saturating_subsume(imbalance);
468
469 let fee = WeightToFee::weight_to_fee(&weight);
470 let (credit_out, credit_change) = match SwapCredit::swap_tokens_for_exact_tokens(
472 vec![swap_asset, Target::get()],
473 credit_in,
474 fee,
475 ) {
476 Ok(a) => a,
477 Err((credit_in, error)) => {
478 log::trace!(
479 target: "xcm::weight",
480 "SwapFirstAssetTrader::buy_weight swap couldn't be done. Error was: {:?}",
481 error,
482 );
483 let taken =
485 AssetsInHolding::new_from_fungible_credit(id.clone(), Box::new(credit_in));
486 payment.subsume_assets(taken);
487 return Err((payment, XcmError::FeesNotMet));
488 },
489 };
490
491 match self.total_fee.subsume(credit_out) {
492 Err(credit_out) => {
493 defensive!(
496 "`total_fee.asset` must be equal to `credit_out.asset`",
497 (self.total_fee.asset(), credit_out.asset())
498 );
499 return Err((payment, XcmError::FeesNotMet));
500 },
501 _ => (),
502 };
503 self.last_fee_asset = Some(id.clone());
504
505 if credit_change.peek() != Zero::zero() {
506 let unspent = AssetsInHolding::new_from_fungible_credit(id, Box::new(credit_change));
507 payment.subsume_assets(unspent);
508 }
509 Ok(payment)
510 }
511
512 fn refund_weight(&mut self, weight: Weight, _context: &XcmContext) -> Option<AssetsInHolding> {
513 log::trace!(
514 target: "xcm::weight",
515 "SwapFirstAssetTrader::refund_weight weight: {:?}, self.total_fee: {:?}",
516 weight,
517 self.total_fee,
518 );
519 if weight.is_zero() || self.total_fee.peek().is_zero() {
520 return None;
522 }
523 let refund_asset = if let Some(asset) = &self.last_fee_asset {
524 (asset.clone(), Fungible(0)).into()
526 } else {
527 return None;
528 };
529 let refund_amount = WeightToFee::weight_to_fee(&weight);
530 if refund_amount >= self.total_fee.peek() {
531 return None;
533 }
534
535 let refund_swap_asset = FungiblesAssetMatcher::matches_fungibles(&refund_asset)
536 .map(|(a, _)| a.into())
537 .ok()?;
538
539 let refund = self.total_fee.extract(refund_amount);
540 let refund = match SwapCredit::swap_exact_tokens_for_tokens(
541 vec![Target::get(), refund_swap_asset],
542 refund,
543 None,
544 ) {
545 Ok(refund_in_target) => refund_in_target,
546 Err((refund, _)) => {
547 let _ = self.total_fee.subsume(refund).map_err(|refund| {
549 defensive!(
552 "`total_fee.asset` must be equal to `refund.asset`",
553 (self.total_fee.asset(), refund.asset())
554 );
555 });
556 return None;
557 },
558 };
559
560 let refund = AssetsInHolding::new_from_fungible_credit(refund_asset.id, Box::new(refund));
561 Some(refund)
562 }
563
564 fn quote_weight(
565 &mut self,
566 weight: Weight,
567 given_id: AssetId,
568 _context: &XcmContext,
569 ) -> Result<Asset, XcmError> {
570 log::trace!(
571 target: "xcm::weight",
572 "SwapFirstAssetTrader::quote_weight weight: {:?}, given_id: {:?}",
573 weight,
574 given_id,
575 );
576 if weight.is_zero() {
577 return Err(XcmError::NoDeal);
578 }
579
580 let give_matcher: Asset = (given_id.clone(), 1).into();
581 let (give_fungibles_id, _) = FungiblesAssetMatcher::matches_fungibles(&give_matcher)
582 .map_err(|_| XcmError::AssetNotFound)?;
583 let want_fungibles_id = Target::get();
584 if give_fungibles_id.eq(&want_fungibles_id.clone().into()) {
585 return Err(XcmError::FeesNotMet);
586 }
587
588 let want_amount = WeightToFee::weight_to_fee(&weight);
589 let necessary_give: u128 = <SwapCredit as QuotePrice>::quote_price_tokens_for_exact_tokens(
591 give_fungibles_id,
592 want_fungibles_id,
593 want_amount,
594 true, )
596 .filter(|amount| *amount > 0u128.into())
597 .ok_or(XcmError::FeesNotMet)?
598 .into();
599 Ok((given_id, necessary_give).into())
600 }
601}
602
603impl<
604 Target: Get<Fungibles::AssetId>,
605 SwapCredit: SwapCreditT<
606 AccountId,
607 Balance = Fungibles::Balance,
608 AssetKind = Fungibles::AssetId,
609 Credit = fungibles::Credit<AccountId, Fungibles>,
610 > + QuotePrice,
611 WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
612 Fungibles: fungibles::Balanced<AccountId>,
613 FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
614 OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
615 AccountId,
616 > Drop
617 for SwapFirstAssetTrader<
618 Target,
619 SwapCredit,
620 WeightToFee,
621 Fungibles,
622 FungiblesAssetMatcher,
623 OnUnbalanced,
624 AccountId,
625 >
626where
627 Fungibles::Balance: From<u128> + Into<u128>,
628{
629 fn drop(&mut self) {
630 if self.total_fee.peek().is_zero() {
631 return;
632 }
633 let total_fee = self.total_fee.extract(self.total_fee.peek());
634 OnUnbalanced::on_unbalanced(total_fee);
635 }
636}
637
638#[cfg(test)]
639mod test_xcm_router {
640 use super::*;
641 use cumulus_primitives_core::UpwardMessage;
642 use frame_support::assert_ok;
643 use xcm::MAX_XCM_DECODE_DEPTH;
644
645 struct OkFixedXcmHashWithAssertingRequiredInputsSender;
647
648 impl OkFixedXcmHashWithAssertingRequiredInputsSender {
649 const FIXED_XCM_HASH: [u8; 32] = [9; 32];
650
651 fn fixed_delivery_asset() -> Assets {
652 Assets::new()
653 }
654
655 fn expected_delivery_result() -> Result<(XcmHash, Assets), SendError> {
656 Ok((Self::FIXED_XCM_HASH, Self::fixed_delivery_asset()))
657 }
658 }
659
660 impl SendXcm for OkFixedXcmHashWithAssertingRequiredInputsSender {
661 type Ticket = ();
662
663 fn validate(
664 destination: &mut Option<Location>,
665 message: &mut Option<Xcm<()>>,
666 ) -> SendResult<Self::Ticket> {
667 assert!(destination.is_some());
668 assert!(message.is_some());
669 Ok(((), OkFixedXcmHashWithAssertingRequiredInputsSender::fixed_delivery_asset()))
670 }
671
672 fn deliver(_: Self::Ticket) -> Result<XcmHash, SendError> {
673 Ok(Self::FIXED_XCM_HASH)
674 }
675 }
676
677 struct CanSendUpwardMessageSender;
679
680 impl UpwardMessageSender for CanSendUpwardMessageSender {
681 fn send_upward_message(_: UpwardMessage) -> Result<(u32, XcmHash), MessageSendError> {
682 Err(MessageSendError::Other)
683 }
684
685 fn can_send_upward_message(_: &UpwardMessage) -> Result<(), MessageSendError> {
686 Ok(())
687 }
688 }
689
690 #[test]
691 fn parent_as_ump_does_not_consume_dest_or_msg_on_not_applicable() {
692 let message = Xcm(vec![Trap(5)]);
694
695 let dest = (Parent, Parent, Parent);
697 let mut dest_wrapper = Some(dest.into());
698 let mut msg_wrapper = Some(message.clone());
699 assert_eq!(
700 Err(SendError::NotApplicable),
701 <ParentAsUmp<(), (), ()> as SendXcm>::validate(&mut dest_wrapper, &mut msg_wrapper)
702 );
703
704 assert_eq!(Some(dest.into()), dest_wrapper.take());
706 assert_eq!(Some(message.clone()), msg_wrapper.take());
707
708 assert_eq!(
710 OkFixedXcmHashWithAssertingRequiredInputsSender::expected_delivery_result(),
711 send_xcm::<(ParentAsUmp<(), (), ()>, OkFixedXcmHashWithAssertingRequiredInputsSender)>(
712 dest.into(),
713 message,
714 )
715 );
716 }
717
718 #[test]
719 fn parent_as_ump_consumes_dest_and_msg_on_ok_validate() {
720 let message = Xcm(vec![Trap(5)]);
722
723 let dest = (Parent, Here);
725 let mut dest_wrapper = Some(dest.clone().into());
726 let mut msg_wrapper = Some(message.clone());
727 assert!(<ParentAsUmp<CanSendUpwardMessageSender, (), ()> as SendXcm>::validate(
728 &mut dest_wrapper,
729 &mut msg_wrapper,
730 )
731 .is_ok());
732
733 assert_eq!(None, dest_wrapper.take());
735 assert_eq!(None, msg_wrapper.take());
736
737 assert_eq!(
739 Err(SendError::Transport("Other")),
740 send_xcm::<(
741 ParentAsUmp<CanSendUpwardMessageSender, (), ()>,
742 OkFixedXcmHashWithAssertingRequiredInputsSender
743 )>(dest.into(), message)
744 );
745 }
746
747 #[test]
748 fn parent_as_ump_validate_nested_xcm_works() {
749 let dest = Parent;
750
751 type Router = ParentAsUmp<CanSendUpwardMessageSender, (), ()>;
752
753 let mut good = Xcm(vec![ClearOrigin]);
755 for _ in 0..MAX_XCM_DECODE_DEPTH - 1 {
756 good = Xcm(vec![SetAppendix(good)]);
757 }
758
759 assert_ok!(<Router as SendXcm>::validate(&mut Some(dest.into()), &mut Some(good.clone())));
761
762 let bad = Xcm(vec![SetAppendix(good)]);
764 assert_eq!(
765 Err(SendError::ExceedsMaxMessageSize),
766 <Router as SendXcm>::validate(&mut Some(dest.into()), &mut Some(bad))
767 );
768 }
769}
770
771#[cfg(test)]
772mod test_trader {
773 use super::{test_helpers::asset_to_holding, *};
774 use frame_support::{
775 assert_ok,
776 traits::tokens::{
777 DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence,
778 },
779 };
780 use sp_runtime::DispatchError;
781 use xcm_builder::TakeRevenue;
782 use xcm_executor::traits::Error;
783
784 #[test]
785 fn take_first_asset_trader_buy_weight_called_twice_throws_error() {
786 const AMOUNT: u128 = 100;
787
788 type TestAccountId = u32;
790 type TestAssetId = Location; type TestBalance = u128;
792
793 struct TestAssets;
794 impl MatchesFungibles<TestAssetId, TestBalance> for TestAssets {
795 fn matches_fungibles(a: &Asset) -> Result<(TestAssetId, TestBalance), Error> {
796 match a {
797 Asset { fun: Fungible(amount), id: AssetId(_id) } => {
798 Ok((Location::new(0, [GeneralIndex(1)]), *amount))
799 },
800 _ => Err(Error::AssetNotHandled),
801 }
802 }
803 }
804 impl fungibles::Inspect<TestAccountId> for TestAssets {
805 type AssetId = TestAssetId;
806 type Balance = TestBalance;
807
808 fn total_issuance(_: Self::AssetId) -> Self::Balance {
809 0
810 }
811
812 fn minimum_balance(_: Self::AssetId) -> Self::Balance {
813 0
814 }
815
816 fn balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
817 0
818 }
819
820 fn total_balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
821 0
822 }
823
824 fn reducible_balance(
825 _: Self::AssetId,
826 _: &TestAccountId,
827 _: Preservation,
828 _: Fortitude,
829 ) -> Self::Balance {
830 0
831 }
832
833 fn can_deposit(
834 _: Self::AssetId,
835 _: &TestAccountId,
836 _: Self::Balance,
837 _: Provenance,
838 ) -> DepositConsequence {
839 DepositConsequence::Success
840 }
841
842 fn can_withdraw(
843 _: Self::AssetId,
844 _: &TestAccountId,
845 _: Self::Balance,
846 ) -> WithdrawConsequence<Self::Balance> {
847 WithdrawConsequence::Success
848 }
849
850 fn asset_exists(_: Self::AssetId) -> bool {
851 true
852 }
853 }
854 impl fungibles::Mutate<TestAccountId> for TestAssets {}
855 impl fungibles::Balanced<TestAccountId> for TestAssets {
856 type OnDropCredit = fungibles::DecreaseIssuance<TestAccountId, Self>;
857 type OnDropDebt = fungibles::IncreaseIssuance<TestAccountId, Self>;
858 }
859 impl fungibles::Unbalanced<TestAccountId> for TestAssets {
860 fn handle_dust(_: fungibles::Dust<TestAccountId, Self>) {}
861 fn write_balance(
862 _: Self::AssetId,
863 _: &TestAccountId,
864 _: Self::Balance,
865 ) -> Result<Option<Self::Balance>, DispatchError> {
866 Ok(None)
867 }
868
869 fn set_total_issuance(_: Self::AssetId, _: Self::Balance) {}
870 }
871
872 struct FeeChargerAssetsHandleRefund;
873 impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeChargerAssetsHandleRefund {
874 fn charge_weight_in_fungibles(
875 _: <TestAssets as fungibles::Inspect<TestAccountId>>::AssetId,
876 _: Weight,
877 ) -> Result<<TestAssets as fungibles::Inspect<TestAccountId>>::Balance, XcmError> {
878 Ok(AMOUNT)
879 }
880 }
881 impl TakeRevenue for FeeChargerAssetsHandleRefund {
882 fn take_revenue(_: AssetsInHolding) {}
883 }
884
885 struct HandleFees;
887 impl OnUnbalancedT<fungibles::Credit<TestAccountId, TestAssets>> for HandleFees {
888 fn on_unbalanced(_: fungibles::Credit<TestAccountId, TestAssets>) {
889 }
891 }
892
893 type Trader = TakeFirstAssetTrader<
895 TestAccountId,
896 FeeChargerAssetsHandleRefund,
897 TestAssets,
898 TestAssets,
899 HandleFees,
900 >;
901 let mut trader = <Trader as WeightTrader>::new();
902 let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
903
904 let asset: Asset = (Here, AMOUNT).into();
906 let payment1 = asset_to_holding(asset.clone());
907 let payment2 = asset_to_holding(asset);
908 let weight_to_buy = Weight::from_parts(1_000, 1_000);
909
910 assert_ok!(trader.buy_weight(weight_to_buy, payment1, &ctx));
912
913 let (_, error) = trader.buy_weight(weight_to_buy, payment2, &ctx).unwrap_err();
915 assert_eq!(error, XcmError::NotWithdrawable);
916 }
917
918 #[test]
919 fn take_first_asset_trader_returns_unused_amount() {
920 const REQUIRED_AMOUNT: u128 = 100;
923 const TOTAL_AMOUNT: u128 = 500; type TestAccountId = u32;
927 type TestAssetId = Location;
928 type TestBalance = u128;
929
930 struct TestAssets;
931 impl MatchesFungibles<TestAssetId, TestBalance> for TestAssets {
932 fn matches_fungibles(a: &Asset) -> Result<(TestAssetId, TestBalance), Error> {
933 match a {
934 Asset { fun: Fungible(amount), id: AssetId(_id) } => {
935 Ok((Location::new(0, [GeneralIndex(1)]), *amount))
936 },
937 _ => Err(Error::AssetNotHandled),
938 }
939 }
940 }
941 impl fungibles::Inspect<TestAccountId> for TestAssets {
942 type AssetId = TestAssetId;
943 type Balance = TestBalance;
944
945 fn total_issuance(_: Self::AssetId) -> Self::Balance {
946 0
947 }
948
949 fn minimum_balance(_: Self::AssetId) -> Self::Balance {
950 0
951 }
952
953 fn balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
954 0
955 }
956
957 fn total_balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
958 0
959 }
960
961 fn reducible_balance(
962 _: Self::AssetId,
963 _: &TestAccountId,
964 _: Preservation,
965 _: Fortitude,
966 ) -> Self::Balance {
967 0
968 }
969
970 fn can_deposit(
971 _: Self::AssetId,
972 _: &TestAccountId,
973 _: Self::Balance,
974 _: Provenance,
975 ) -> DepositConsequence {
976 DepositConsequence::Success
977 }
978
979 fn can_withdraw(
980 _: Self::AssetId,
981 _: &TestAccountId,
982 _: Self::Balance,
983 ) -> WithdrawConsequence<Self::Balance> {
984 WithdrawConsequence::Success
985 }
986
987 fn asset_exists(_: Self::AssetId) -> bool {
988 true
989 }
990 }
991 impl fungibles::Mutate<TestAccountId> for TestAssets {}
992 impl fungibles::Balanced<TestAccountId> for TestAssets {
993 type OnDropCredit = fungibles::DecreaseIssuance<TestAccountId, Self>;
994 type OnDropDebt = fungibles::IncreaseIssuance<TestAccountId, Self>;
995 }
996 impl fungibles::Unbalanced<TestAccountId> for TestAssets {
997 fn handle_dust(_: fungibles::Dust<TestAccountId, Self>) {}
998 fn write_balance(
999 _: Self::AssetId,
1000 _: &TestAccountId,
1001 _: Self::Balance,
1002 ) -> Result<Option<Self::Balance>, DispatchError> {
1003 Ok(None)
1004 }
1005
1006 fn set_total_issuance(_: Self::AssetId, _: Self::Balance) {}
1007 }
1008
1009 struct FeeChargerAssetsHandleRefund;
1010 impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeChargerAssetsHandleRefund {
1011 fn charge_weight_in_fungibles(
1012 _: <TestAssets as fungibles::Inspect<TestAccountId>>::AssetId,
1013 _: Weight,
1014 ) -> Result<<TestAssets as fungibles::Inspect<TestAccountId>>::Balance, XcmError> {
1015 Ok(REQUIRED_AMOUNT)
1016 }
1017 }
1018 impl TakeRevenue for FeeChargerAssetsHandleRefund {
1019 fn take_revenue(_: AssetsInHolding) {}
1020 }
1021
1022 struct HandleFees;
1023 impl OnUnbalancedT<fungibles::Credit<TestAccountId, TestAssets>> for HandleFees {
1024 fn on_unbalanced(_: fungibles::Credit<TestAccountId, TestAssets>) {}
1025 }
1026
1027 type Trader = TakeFirstAssetTrader<
1029 TestAccountId,
1030 FeeChargerAssetsHandleRefund,
1031 TestAssets,
1032 TestAssets,
1033 HandleFees,
1034 >;
1035 let mut trader = <Trader as WeightTrader>::new();
1036 let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
1037
1038 let asset: Asset = (Here, TOTAL_AMOUNT).into();
1040 let payment = asset_to_holding(asset.clone());
1041 let weight_to_buy = Weight::from_parts(1_000, 1_000);
1042
1043 let result = trader.buy_weight(weight_to_buy, payment, &ctx);
1045 assert_ok!(&result);
1046
1047 let unused_payment = result.unwrap();
1048
1049 let expected_excess = TOTAL_AMOUNT - REQUIRED_AMOUNT;
1051 let unused_assets: Vec<Asset> = unused_payment.fungible_assets_iter().collect();
1052
1053 assert_eq!(unused_assets.len(), 1);
1055
1056 match &unused_assets[0] {
1058 Asset { fun: Fungible(amount), .. } => {
1059 assert_eq!(*amount, expected_excess, "Expected excess amount to be returned");
1060 },
1061 _ => panic!("Expected fungible asset"),
1062 }
1063 }
1064
1065 #[test]
1066 fn take_first_asset_trader_refund_keeps_ed_for_drop_handler() {
1067 use core::cell::Cell;
1071
1072 const ED: u128 = 10;
1073 const BUY_FEE: u128 = 15;
1074 const REFUND_FEE: u128 = 10;
1075
1076 type TestAccountId = u32;
1077 type TestAssetId = Location;
1078 type TestBalance = u128;
1079
1080 struct TestAssets;
1081 impl MatchesFungibles<TestAssetId, TestBalance> for TestAssets {
1082 fn matches_fungibles(a: &Asset) -> Result<(TestAssetId, TestBalance), Error> {
1083 match a {
1084 Asset { fun: Fungible(amount), id: AssetId(_) } => {
1085 Ok((Location::new(0, [GeneralIndex(1)]), *amount))
1086 },
1087 _ => Err(Error::AssetNotHandled),
1088 }
1089 }
1090 }
1091 impl fungibles::Inspect<TestAccountId> for TestAssets {
1092 type AssetId = TestAssetId;
1093 type Balance = TestBalance;
1094 fn total_issuance(_: Self::AssetId) -> Self::Balance {
1095 0
1096 }
1097 fn minimum_balance(_: Self::AssetId) -> Self::Balance {
1098 ED
1099 }
1100 fn balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
1101 0
1102 }
1103 fn total_balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
1104 0
1105 }
1106 fn reducible_balance(
1107 _: Self::AssetId,
1108 _: &TestAccountId,
1109 _: Preservation,
1110 _: Fortitude,
1111 ) -> Self::Balance {
1112 0
1113 }
1114 fn can_deposit(
1115 _: Self::AssetId,
1116 _: &TestAccountId,
1117 _: Self::Balance,
1118 _: Provenance,
1119 ) -> DepositConsequence {
1120 DepositConsequence::Success
1121 }
1122 fn can_withdraw(
1123 _: Self::AssetId,
1124 _: &TestAccountId,
1125 _: Self::Balance,
1126 ) -> WithdrawConsequence<Self::Balance> {
1127 WithdrawConsequence::Success
1128 }
1129 fn asset_exists(_: Self::AssetId) -> bool {
1130 true
1131 }
1132 }
1133 impl fungibles::Mutate<TestAccountId> for TestAssets {}
1134 impl fungibles::Balanced<TestAccountId> for TestAssets {
1135 type OnDropCredit = fungibles::DecreaseIssuance<TestAccountId, Self>;
1136 type OnDropDebt = fungibles::IncreaseIssuance<TestAccountId, Self>;
1137 }
1138 impl fungibles::Unbalanced<TestAccountId> for TestAssets {
1139 fn handle_dust(_: fungibles::Dust<TestAccountId, Self>) {}
1140 fn write_balance(
1141 _: Self::AssetId,
1142 _: &TestAccountId,
1143 _: Self::Balance,
1144 ) -> Result<Option<Self::Balance>, DispatchError> {
1145 Ok(None)
1146 }
1147 fn set_total_issuance(_: Self::AssetId, _: Self::Balance) {}
1148 }
1149
1150 thread_local! {
1153 static CALLS: Cell<u32> = const { Cell::new(0) };
1154 static HANDLER_RECEIVED: Cell<u128> = const { Cell::new(0) };
1155 }
1156 struct FeeCharger;
1157 impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeCharger {
1158 fn charge_weight_in_fungibles(
1159 _: <TestAssets as fungibles::Inspect<TestAccountId>>::AssetId,
1160 _: Weight,
1161 ) -> Result<<TestAssets as fungibles::Inspect<TestAccountId>>::Balance, XcmError> {
1162 let n = CALLS.with(|c| {
1163 let v = c.get();
1164 c.set(v + 1);
1165 v
1166 });
1167 Ok(if n == 0 { BUY_FEE } else { REFUND_FEE })
1168 }
1169 }
1170
1171 struct HandleFees;
1172 impl OnUnbalancedT<fungibles::Credit<TestAccountId, TestAssets>> for HandleFees {
1173 fn on_unbalanced(credit: fungibles::Credit<TestAccountId, TestAssets>) {
1174 HANDLER_RECEIVED.with(|h| h.set(credit.peek()));
1175 }
1176 }
1177
1178 type Trader =
1179 TakeFirstAssetTrader<TestAccountId, FeeCharger, TestAssets, TestAssets, HandleFees>;
1180 let mut trader = <Trader as WeightTrader>::new();
1181 let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
1182
1183 let payment = asset_to_holding((Here, BUY_FEE).into());
1185 assert_ok!(trader.buy_weight(Weight::from_parts(1_000, 1_000), payment, &ctx));
1186
1187 let refund = trader
1190 .refund_weight(Weight::from_parts(500, 500), &ctx)
1191 .expect("non-zero refund");
1192 let refund_assets: Vec<Asset> = refund.fungible_assets_iter().collect();
1193 assert_eq!(refund_assets.len(), 1);
1194 match &refund_assets[0] {
1195 Asset { fun: Fungible(amount), .. } => {
1196 assert_eq!(*amount, BUY_FEE - ED, "refund must be `outstanding - ED`, not ED")
1197 },
1198 _ => panic!("expected fungible refund"),
1199 }
1200
1201 drop(trader);
1203 HANDLER_RECEIVED.with(|h| {
1204 assert_eq!(h.get(), ED, "OnUnbalanced drop handler must receive at least ED")
1205 });
1206 }
1207}
1208
1209#[cfg(feature = "runtime-benchmarks")]
1215pub struct ToParentDeliveryHelper<XcmConfig, ExistentialDeposit, PriceForDelivery>(
1216 core::marker::PhantomData<(XcmConfig, ExistentialDeposit, PriceForDelivery)>,
1217);
1218
1219#[cfg(feature = "runtime-benchmarks")]
1220impl<
1221 XcmConfig: xcm_executor::Config,
1222 ExistentialDeposit: Get<Option<Asset>>,
1223 PriceForDelivery: PriceForMessageDelivery<Id = ()>,
1224 > xcm_builder::EnsureDelivery
1225 for ToParentDeliveryHelper<XcmConfig, ExistentialDeposit, PriceForDelivery>
1226{
1227 fn ensure_successful_delivery(
1228 origin_ref: &Location,
1229 dest: &Location,
1230 fee_reason: xcm_executor::traits::FeeReason,
1231 ) -> (Option<xcm_executor::FeesMode>, Option<Assets>) {
1232 use xcm::{latest::MAX_ITEMS_IN_ASSETS, MAX_INSTRUCTIONS_TO_DECODE};
1233 use xcm_executor::{
1234 traits::{FeeManager, TransactAsset},
1235 FeesMode,
1236 };
1237
1238 if dest.ne(&Location::parent()) {
1240 return (None, None);
1241 }
1242
1243 XcmConfig::XcmSender::ensure_successful_delivery(Some(Location::parent()));
1245
1246 let mut fees_mode = None;
1247 if !XcmConfig::FeeManager::is_waived(Some(origin_ref), fee_reason) {
1248 let context = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
1250
1251 if let Some(ed) = ExistentialDeposit::get() {
1253 let holdings = XcmConfig::AssetTransactor::mint_asset(&ed, &context).unwrap();
1254 XcmConfig::AssetTransactor::deposit_asset(holdings, &origin_ref, Some(&context))
1255 .unwrap();
1256 }
1257
1258 let mut max_assets: Vec<Asset> = Vec::new();
1260 for i in 0..MAX_ITEMS_IN_ASSETS {
1261 max_assets.push((GeneralIndex(i as u128), 100u128).into());
1262 }
1263 let overestimated_xcm =
1264 vec![WithdrawAsset(max_assets.into()); MAX_INSTRUCTIONS_TO_DECODE as usize].into();
1265 let overestimated_fees = PriceForDelivery::price_for_delivery((), &overestimated_xcm);
1266
1267 for fee in overestimated_fees.inner() {
1269 let holdings = XcmConfig::AssetTransactor::mint_asset(fee, &context).unwrap();
1270 XcmConfig::AssetTransactor::deposit_asset(holdings, &origin_ref, Some(&context))
1271 .unwrap();
1272 }
1273
1274 fees_mode = Some(FeesMode { jit_withdraw: true });
1276 }
1277 (fees_mode, None)
1278 }
1279}