diff --git a/xtokens/README.md b/xtokens/README.md index 881ef2362..df059782a 100644 --- a/xtokens/README.md +++ b/xtokens/README.md @@ -28,3 +28,49 @@ Integration tests could be done manually after integrating orml-xtokens into run - Sending the tx from parachain A. - Set the destination as Parachain B. - Set the currency ID as parachain C token. + + +#### Transfer multiple currencies + +- Transfer relay chain tokens to relay chain, and use relay chain token as fee +- Transfer relay chain tokens to parachain, and use relay chain token as fee +- Transfer tokens issued by parachain A, from parachain A to parachain B, and use parachain A token as fee +- Transfer tokens issued by parachain B, from parachain A to parachain B, and use parachain B token as fee +- Transfer tokens issued by parachain C, from parachain A to parachain B, and use parachain C token as fee +- Transfer tokens issued by parachain B, from parachain A to parachain B, and use relay chain token as fee + +Notice, in the case of parachain A transfer parachain B token to parachain B, and use relay chain token as fee. Because fee asset is relaychain token, and non fee asset is parachain B token, this is two different chain. We call chain of fee asset as fee_reserve, and chain of non fee asset as non_fee_reserve. And in this case fee_reserve location is also refer to destination parachain. + +The current implementation is sent two xcm from sender parachain. first xcm sent to fee reserve chain which will also route xcm message to destination parachain. second xcm directly sent to destination parachain. + +the fee amount in fee asset is split into two parts. +1. fee asset sent to fee reserve chain = fee_amount - min_xcm_fee +2. fee asset sent to dest reserve chain = min_xcm_fee + +Parachains should implements config `MinXcmFee` in `xtokens` module config: + +```rust +parameter_type_with_key! { + pub ParachainMinFee: |location: MultiLocation| -> u128 { + #[allow(clippy::match_ref_pats)] // false positive + match (location.parents, location.first_interior()) { + (1, Some(Parachain(parachains::statemine::ID))) => 4_000_000_000, + _ => u128::MAX, + } + }; +} +``` + +If Parachain don't want have this case, can simply return Max value: + +```rust +parameter_type_with_key! { + pub ParachainMinFee: |_location: MultiLocation| -> u128 { + u128::MAX + }; +} +``` + +Notice the implementation for now also rely on `SelfLocation` which already in `xtokens` config. The `SelfLocation` current is `(1, Parachain(id))` refer to sender parachain. If parachain set `SelfLocation` to (0, Here), it'll be error in this case. + +We use `SelfLocation` to fund fee to sender's parachain sovereign account on destination parachain, which asset is originated from sender account on sender parachain. This means if user setup too much fee, the fee will not returned to user, instead deposit to sibling parachain sovereign account on destination parachain. \ No newline at end of file diff --git a/xtokens/src/lib.rs b/xtokens/src/lib.rs index 5a6fd3810..3eb0e416d 100644 --- a/xtokens/src/lib.rs +++ b/xtokens/src/lib.rs @@ -36,7 +36,7 @@ use xcm_executor::traits::{InvertLocation, WeightBounds}; pub use module::*; use orml_traits::{ location::{Parse, Reserve}, - XcmTransfer, + GetByKey, XcmTransfer, }; mod mock; @@ -54,7 +54,6 @@ use TransferKind::*; #[frame_support::pallet] pub mod module { - use super::*; #[pallet::config] @@ -83,6 +82,9 @@ pub mod module { #[pallet::constant] type SelfLocation: Get; + /// Minimum xcm execution fee paid on destination chain. + type MinXcmFee: GetByKey; + /// XCM executor. type XcmExecutor: ExecuteXcm; @@ -144,16 +146,18 @@ pub mod module { /// interpreted. BadVersion, /// We tried sending distinct asset and fee but they have different - /// reserve chains + /// reserve chains. DistinctReserveForAssetAndFee, /// The fee is zero. ZeroFee, /// The transfering asset amount is zero. ZeroAmount, - /// The number of assets to be sent is over the maximum + /// The number of assets to be sent is over the maximum. TooManyAssetsBeingSent, - /// The specified index does not exist in a MultiAssets struct + /// The specified index does not exist in a MultiAssets struct. AssetIndexNonExistent, + /// Fee is not enough. + FeeNotEnough, } #[pallet::hooks] @@ -467,43 +471,122 @@ pub mod module { assets.len() <= T::MaxAssetsForTransfer::get(), Error::::TooManyAssetsBeingSent ); + let origin_location = T::AccountIdToMultiLocation::convert(who.clone()); - // We check that all assets are valid and share the same reserve - for i in 0..assets.len() { + let mut non_fee_reserve: Option = None; + let asset_len = assets.len(); + for i in 0..asset_len { let asset = assets.get(i).ok_or(Error::::AssetIndexNonExistent)?; ensure!( matches!(asset.fun, Fungibility::Fungible(x) if !x.is_zero()), Error::::InvalidAsset ); - ensure!( - fee.reserve() == asset.reserve(), - Error::::DistinctReserveForAssetAndFee - ); + // `assets` includes fee, the reserve location is decided by non fee asset + if (fee != *asset && non_fee_reserve.is_none()) || asset_len == 1 { + non_fee_reserve = asset.reserve(); + } + // make sure all non fee assets share the same reserve + if non_fee_reserve.is_some() { + ensure!( + non_fee_reserve == asset.reserve(), + Error::::DistinctReserveForAssetAndFee + ); + } } - let (transfer_kind, dest, reserve, recipient) = Self::transfer_kind(&fee, &dest)?; - let mut msg = match transfer_kind { - SelfReserveAsset => Self::transfer_self_reserve_asset( - assets.clone(), - fee.clone(), - dest.clone(), - recipient, - dest_weight, - )?, - ToReserve => { - Self::transfer_to_reserve(assets.clone(), fee.clone(), dest.clone(), recipient, dest_weight)? + let fee_reserve = fee.reserve(); + if fee_reserve != non_fee_reserve { + // Current only support `ToReserve` with relay-chain asset as fee. other case + // like `NonReserve` or `SelfReserve` with relay-chain fee is not support. + ensure!(non_fee_reserve == dest.chain_part(), Error::::InvalidAsset); + + let reserve_location = non_fee_reserve.clone().ok_or(Error::::AssetHasNoReserve)?; + let min_xcm_fee = T::MinXcmFee::get(&reserve_location); + + // min xcm fee should less than user fee + let fee_to_dest: MultiAsset = (fee.id.clone(), min_xcm_fee).into(); + ensure!(fee_to_dest < fee, Error::::FeeNotEnough); + + let mut assets_to_dest = MultiAssets::new(); + for i in 0..asset_len { + let asset = assets.get(i).ok_or(Error::::AssetIndexNonExistent)?; + if fee != *asset { + assets_to_dest.push(asset.clone()); + } else { + assets_to_dest.push(fee_to_dest.clone()); + } } - ToNonReserve => Self::transfer_to_non_reserve( + + let mut assets_to_fee_reserve = MultiAssets::new(); + let asset_to_fee_reserve = subtract_fee(&fee, min_xcm_fee); + assets_to_fee_reserve.push(asset_to_fee_reserve.clone()); + + // First xcm sent to fee reserve chain and routed to dest chain. + Self::execute_and_send_reserve_kind_xcm( + origin_location.clone(), + assets_to_fee_reserve, + asset_to_fee_reserve, + fee_reserve, + &dest, + Some(T::SelfLocation::get()), + dest_weight, + )?; + + // Second xcm send to dest chain. + Self::execute_and_send_reserve_kind_xcm( + origin_location, + assets_to_dest, + fee_to_dest, + non_fee_reserve, + &dest, + None, + dest_weight, + )?; + } else { + Self::execute_and_send_reserve_kind_xcm( + origin_location, assets.clone(), fee.clone(), - reserve, - dest.clone(), - recipient, + non_fee_reserve, + &dest, + None, dest_weight, - )?, + )?; + } + + Self::deposit_event(Event::::TransferredMultiAssets { + sender: who, + assets, + fee, + dest, + }); + + Ok(()) + } + + /// Execute and send xcm with given assets and fee to dest chain or + /// reserve chain. + fn execute_and_send_reserve_kind_xcm( + origin_location: MultiLocation, + assets: MultiAssets, + fee: MultiAsset, + reserve: Option, + dest: &MultiLocation, + maybe_recipient_override: Option, + dest_weight: Weight, + ) -> DispatchResult { + let (transfer_kind, dest, reserve, recipient) = Self::transfer_kind(reserve, dest)?; + let recipient = match maybe_recipient_override { + Some(recipient) => recipient, + None => recipient, + }; + + let mut msg = match transfer_kind { + SelfReserveAsset => Self::transfer_self_reserve_asset(assets, fee, dest, recipient, dest_weight)?, + ToReserve => Self::transfer_to_reserve(assets, fee, dest, recipient, dest_weight)?, + ToNonReserve => Self::transfer_to_non_reserve(assets, fee, reserve, dest, recipient, dest_weight)?, }; - let origin_location = T::AccountIdToMultiLocation::convert(who.clone()); let weight = T::Weigher::weight(&mut msg).map_err(|()| Error::::UnweighableMessage)?; T::XcmExecutor::execute_xcm_in_credit(origin_location, msg, weight, weight) .ensure_complete() @@ -512,13 +595,6 @@ pub mod module { Error::::XcmExecutionFailed })?; - Self::deposit_event(Event::::TransferredMultiAssets { - sender: who, - assets, - fee, - dest, - }); - Ok(()) } @@ -639,14 +715,14 @@ pub mod module { /// Get the transfer kind. /// - /// Returns `Err` if `asset` and `dest` combination doesn't make sense, - /// else returns a tuple of: + /// Returns `Err` if `dest` combination doesn't make sense, or `reserve` + /// is none, else returns a tuple of: /// - `transfer_kind`. /// - asset's `reserve` parachain or relay chain location, /// - `dest` parachain or relay chain location. /// - `recipient` location. fn transfer_kind( - asset: &MultiAsset, + reserve: Option, dest: &MultiLocation, ) -> Result<(TransferKind, MultiLocation, MultiLocation, MultiLocation), DispatchError> { let (dest, recipient) = Self::ensure_valid_dest(dest)?; @@ -654,7 +730,7 @@ pub mod module { let self_location = T::SelfLocation::get(); ensure!(dest != self_location, Error::::NotCrossChainTransfer); - let reserve = asset.reserve().ok_or(Error::::AssetHasNoReserve)?; + let reserve = reserve.ok_or(Error::::AssetHasNoReserve)?; let transfer_kind = if reserve == self_location { SelfReserveAsset } else if reserve == dest { @@ -670,13 +746,13 @@ pub mod module { impl Pallet { /// Returns weight of `transfer_multiasset` call. fn weight_of_transfer_multiasset(asset: &VersionedMultiAsset, dest: &VersionedMultiLocation) -> Weight { - let asset = asset.clone().try_into(); + let asset: Result = asset.clone().try_into(); let dest = dest.clone().try_into(); if let (Ok(asset), Ok(dest)) = (asset, dest) { - if let Ok((transfer_kind, dest, _, reserve)) = Self::transfer_kind(&asset, &dest) { + if let Ok((transfer_kind, dest, _, reserve)) = Self::transfer_kind(asset.reserve(), &dest) { let mut msg = match transfer_kind { SelfReserveAsset => Xcm(vec![ - WithdrawAsset(MultiAssets::from(asset.clone())), + WithdrawAsset(MultiAssets::from(asset)), DepositReserveAsset { assets: All.into(), max_assets: 1, @@ -685,7 +761,7 @@ pub mod module { }, ]), ToReserve | ToNonReserve => Xcm(vec![ - WithdrawAsset(MultiAssets::from(asset.clone())), + WithdrawAsset(MultiAssets::from(asset)), InitiateReserveWithdraw { assets: All.into(), // `dest` is always (equal to) `reserve` in both cases @@ -737,38 +813,53 @@ pub mod module { dest: &VersionedMultiLocation, ) -> Weight { let assets: Result = assets.clone().try_into(); - let dest = dest.clone().try_into(); if let (Ok(assets), Ok(dest)) = (assets, dest) { - if let Some(fee) = assets.get(*fee_item as usize) { - if let Ok((transfer_kind, dest, _, reserve)) = Self::transfer_kind(fee, &dest) { - let mut msg = match transfer_kind { - SelfReserveAsset => Xcm(vec![ - WithdrawAsset(assets.clone()), - DepositReserveAsset { - assets: All.into(), - max_assets: assets.len() as u32, - dest, - xcm: Xcm(vec![]), - }, - ]), - ToReserve | ToNonReserve => Xcm(vec![ - WithdrawAsset(assets), - InitiateReserveWithdraw { - assets: All.into(), - // `dest` is always (equal to) `reserve` in both cases - reserve, - xcm: Xcm(vec![]), - }, - ]), - }; - return T::Weigher::weight(&mut msg) - .map_or(Weight::max_value(), |w| T::BaseXcmWeight::get().saturating_add(w)); - } + let reserve_location = Self::get_reserve_location(&assets, fee_item); + // if let Ok(reserve_location) = reserve_location { + if let Ok((transfer_kind, dest, _, reserve)) = Self::transfer_kind(reserve_location, &dest) { + let mut msg = match transfer_kind { + SelfReserveAsset => Xcm(vec![ + WithdrawAsset(assets.clone()), + DepositReserveAsset { + assets: All.into(), + max_assets: assets.len() as u32, + dest, + xcm: Xcm(vec![]), + }, + ]), + ToReserve | ToNonReserve => Xcm(vec![ + WithdrawAsset(assets), + InitiateReserveWithdraw { + assets: All.into(), + // `dest` is always (equal to) `reserve` in both cases + reserve, + xcm: Xcm(vec![]), + }, + ]), + }; + return T::Weigher::weight(&mut msg) + .map_or(Weight::max_value(), |w| T::BaseXcmWeight::get().saturating_add(w)); } + // } } 0 } + + /// Get reserve location by `assets` and `fee_item`. the `assets` + /// includes fee asset and non fee asset. make sure assets have ge one + /// asset. all non fee asset should share same reserve location. + fn get_reserve_location(assets: &MultiAssets, fee_item: &u32) -> Option { + let reserve_idx = if assets.len() == 1 { + 0 + } else if *fee_item == 0 { + 1 + } else { + 0 + }; + let asset = assets.get(reserve_idx); + asset.and_then(|a| a.reserve()) + } } impl XcmTransfer for Pallet { @@ -813,3 +904,11 @@ fn half(asset: &MultiAsset) -> MultiAsset { id: asset.id.clone(), } } + +fn subtract_fee(asset: &MultiAsset, amount: u128) -> MultiAsset { + let final_amount = fungible_amount(asset).checked_sub(amount).expect("fee too low; qed"); + MultiAsset { + fun: Fungible(final_amount), + id: asset.id.clone(), + } +} diff --git a/xtokens/src/mock/mod.rs b/xtokens/src/mock/mod.rs index ff6e2f5b9..4f6fa3065 100644 --- a/xtokens/src/mock/mod.rs +++ b/xtokens/src/mock/mod.rs @@ -29,6 +29,8 @@ pub enum CurrencyId { B, /// Parachain B B1 token B1, + /// Parachain B B2 token + B2, } pub struct CurrencyIdConvert; @@ -40,6 +42,7 @@ impl Convert> for CurrencyIdConvert { CurrencyId::A1 => Some((Parent, Parachain(1), GeneralKey("A1".into())).into()), CurrencyId::B => Some((Parent, Parachain(2), GeneralKey("B".into())).into()), CurrencyId::B1 => Some((Parent, Parachain(2), GeneralKey("B1".into())).into()), + CurrencyId::B2 => Some((Parent, Parachain(2), GeneralKey("B2".into())).into()), } } } @@ -49,6 +52,7 @@ impl Convert> for CurrencyIdConvert { let a1: Vec = "A1".into(); let b: Vec = "B".into(); let b1: Vec = "B1".into(); + let b2: Vec = "B2".into(); if l == MultiLocation::parent() { return Some(CurrencyId::R); } @@ -58,6 +62,7 @@ impl Convert> for CurrencyIdConvert { X2(Parachain(1), GeneralKey(k)) if k == a1 => Some(CurrencyId::A1), X2(Parachain(2), GeneralKey(k)) if k == b => Some(CurrencyId::B), X2(Parachain(2), GeneralKey(k)) if k == b1 => Some(CurrencyId::B1), + X2(Parachain(2), GeneralKey(k)) if k == b1 => Some(CurrencyId::B2), _ => None, }, MultiLocation { parents, interior } if parents == 0 => match interior { @@ -65,6 +70,7 @@ impl Convert> for CurrencyIdConvert { X1(GeneralKey(k)) if k == b => Some(CurrencyId::B), X1(GeneralKey(k)) if k == a1 => Some(CurrencyId::A1), X1(GeneralKey(k)) if k == b1 => Some(CurrencyId::B1), + X1(GeneralKey(k)) if k == b2 => Some(CurrencyId::B2), _ => None, }, _ => None, diff --git a/xtokens/src/mock/para.rs b/xtokens/src/mock/para.rs index ef46b3de3..689fa3940 100644 --- a/xtokens/src/mock/para.rs +++ b/xtokens/src/mock/para.rs @@ -269,7 +269,17 @@ impl Convert for AccountIdToMultiLocation { parameter_types! { pub SelfLocation: MultiLocation = MultiLocation::new(1, X1(Parachain(ParachainInfo::get().into()))); pub const BaseXcmWeight: Weight = 100_000_000; - pub const MaxAssetsForTransfer: usize = 2; + pub const MaxAssetsForTransfer: usize = 3; +} + +parameter_type_with_key! { + pub ParachainMinFee: |location: MultiLocation| -> u128 { + #[allow(clippy::match_ref_pats)] // false positive + match (location.parents, location.first_interior()) { + (1, Some(Parachain(2))) => 40, + _ => u128::MAX, + } + }; } impl orml_xtokens::Config for Runtime { @@ -279,6 +289,7 @@ impl orml_xtokens::Config for Runtime { type CurrencyIdConvert = CurrencyIdConvert; type AccountIdToMultiLocation = AccountIdToMultiLocation; type SelfLocation = SelfLocation; + type MinXcmFee = ParachainMinFee; type XcmExecutor = XcmExecutor; type Weigher = FixedWeightBounds; type BaseXcmWeight = BaseXcmWeight; diff --git a/xtokens/src/tests.rs b/xtokens/src/tests.rs index df32eefc8..bb4ce5f60 100644 --- a/xtokens/src/tests.rs +++ b/xtokens/src/tests.rs @@ -338,7 +338,7 @@ fn send_sibling_asset_to_reserve_sibling_with_fee() { } #[test] -fn send_sibling_asset_to_reserve_sibling_with_distinc_fee() { +fn send_sibling_asset_to_reserve_sibling_with_distinct_fee() { TestNet::reset(); ParaA::execute_with(|| { @@ -384,7 +384,7 @@ fn send_sibling_asset_to_reserve_sibling_with_distinc_fee() { } #[test] -fn send_sibling_asset_to_reserve_sibling_with_distinc_fee_index_works() { +fn send_sibling_asset_to_reserve_sibling_with_distinct_fee_index_works() { TestNet::reset(); ParaA::execute_with(|| { @@ -640,6 +640,235 @@ fn send_self_parachain_asset_to_sibling_with_distinct_fee() { }); } +#[test] +fn sending_sibling_asset_to_reserve_sibling_with_relay_fee_works() { + TestNet::reset(); + + ParaA::execute_with(|| { + assert_ok!(ParaTokens::deposit(CurrencyId::B, &ALICE, 1_000)); + }); + + ParaB::execute_with(|| { + assert_ok!(ParaTokens::deposit(CurrencyId::B, &sibling_a_account(), 1_000)); + }); + + Relay::execute_with(|| { + let _ = RelayBalances::deposit_creating(¶_a_account(), 1_000); + }); + + let fee_amount: u128 = 200; + let weight: u128 = 50; + let dest_weight: u128 = 40; + + ParaA::execute_with(|| { + assert_ok!(ParaXTokens::transfer_multicurrencies( + Some(ALICE).into(), + vec![(CurrencyId::B, 450), (CurrencyId::R, fee_amount)], + 1, + Box::new( + ( + Parent, + Parachain(2), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + }, + ) + .into() + ), + weight as u64, + )); + assert_eq!(550, ParaTokens::free_balance(CurrencyId::B, &ALICE)); + assert_eq!(1000 - fee_amount, ParaTokens::free_balance(CurrencyId::R, &ALICE)); + }); + + Relay::execute_with(|| { + assert_eq!( + 1000 - (fee_amount - dest_weight), + RelayBalances::free_balance(¶_a_account()) + ); + assert_eq!( + fee_amount - dest_weight * 2, + RelayBalances::free_balance(¶_b_account()) + ); + }); + + ParaB::execute_with(|| { + assert_eq!( + fee_amount - dest_weight * 4, + ParaTokens::free_balance(CurrencyId::R, &sibling_a_account()) + ); + + assert_eq!(450, ParaTokens::free_balance(CurrencyId::B, &BOB)); + assert_eq!(0, ParaTokens::free_balance(CurrencyId::R, &BOB)); + }); +} + +#[test] +fn sending_sibling_asset_to_reserve_sibling_with_relay_fee_not_enough() { + TestNet::reset(); + + ParaA::execute_with(|| { + assert_ok!(ParaTokens::deposit(CurrencyId::B, &ALICE, 1_000)); + }); + + ParaB::execute_with(|| { + assert_ok!(ParaTokens::deposit(CurrencyId::B, &sibling_a_account(), 1_000)); + }); + + Relay::execute_with(|| { + let _ = RelayBalances::deposit_creating(¶_a_account(), 1_000); + }); + + let fee_amount: u128 = 159; + let weight: u128 = 50; + let dest_weight: u128 = 40; + + ParaA::execute_with(|| { + assert_ok!(ParaXTokens::transfer_multicurrencies( + Some(ALICE).into(), + vec![(CurrencyId::B, 450), (CurrencyId::R, fee_amount)], + 1, + Box::new( + ( + Parent, + Parachain(2), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + }, + ) + .into() + ), + weight as u64, + )); + assert_eq!(550, ParaTokens::free_balance(CurrencyId::B, &ALICE)); + assert_eq!(1000 - fee_amount, ParaTokens::free_balance(CurrencyId::R, &ALICE)); + }); + + Relay::execute_with(|| { + assert_eq!( + 1000 - (fee_amount - dest_weight), + RelayBalances::free_balance(¶_a_account()) + ); + assert_eq!( + fee_amount - dest_weight * 2, + RelayBalances::free_balance(¶_b_account()) + ); + }); + + ParaB::execute_with(|| { + // after first xcm succeed, sibling_a amount = 159-120=39 + // second xcm failed, so sibling_a amount stay same. + assert_eq!(39, ParaTokens::free_balance(CurrencyId::R, &sibling_a_account())); + + // second xcm failed, so recipient account don't receive any token of B and R. + assert_eq!(0, ParaTokens::free_balance(CurrencyId::B, &BOB)); + assert_eq!(0, ParaTokens::free_balance(CurrencyId::R, &BOB)); + }); +} + +#[test] +fn transfer_asset_with_relay_fee_failed() { + TestNet::reset(); + + // `SelfReserve` with relay-chain as fee not supported. + ParaA::execute_with(|| { + assert_noop!( + ParaXTokens::transfer_multicurrencies( + Some(ALICE).into(), + vec![(CurrencyId::A, 450), (CurrencyId::R, 100)], + 1, + Box::new( + ( + Parent, + Parachain(2), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + }, + ) + .into() + ), + 40, + ), + Error::::InvalidAsset + ); + }); + + // `NonReserve` with relay-chain as fee not supported. + ParaA::execute_with(|| { + assert_noop!( + ParaXTokens::transfer_multicurrencies( + Some(ALICE).into(), + vec![(CurrencyId::B, 450), (CurrencyId::R, 100)], + 1, + Box::new( + ( + Parent, + Parachain(3), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + }, + ) + .into() + ), + 40, + ), + Error::::InvalidAsset + ); + }); + + // User fee is less than `MinXcmFee` + ParaA::execute_with(|| { + assert_noop!( + ParaXTokens::transfer_multicurrencies( + Some(ALICE).into(), + vec![(CurrencyId::B, 450), (CurrencyId::R, 39)], + 1, + Box::new( + ( + Parent, + Parachain(2), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + }, + ) + .into() + ), + 40, + ), + Error::::FeeNotEnough + ); + }); + + // `MinXcmFee` not defined for destination chain + ParaB::execute_with(|| { + assert_noop!( + ParaXTokens::transfer_multicurrencies( + Some(ALICE).into(), + vec![(CurrencyId::A, 450), (CurrencyId::R, 100)], + 1, + Box::new( + ( + Parent, + Parachain(1), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + }, + ) + .into() + ), + 40, + ), + Error::::FeeNotEnough + ); + }); +} + #[test] fn transfer_no_reserve_assets_fails() { TestNet::reset(); @@ -860,7 +1089,7 @@ fn send_with_insufficient_fee_traps_assets() { assert_ok!(ParaTokens::deposit(CurrencyId::A, &ALICE, 1_000)); // ParaB charges 40, but we specify 30 as fee. Assets will be trapped - // Call succedes in paraA + // Call succeed in paraA assert_ok!(ParaXTokens::transfer_with_fee( Some(ALICE).into(), CurrencyId::A, @@ -908,7 +1137,7 @@ fn send_with_fee_should_handle_overflow() { Some(ALICE).into(), CurrencyId::A, u128::MAX, - 1, + 100, Box::new( MultiLocation::new( 1, @@ -930,18 +1159,20 @@ fn send_with_fee_should_handle_overflow() { } #[test] -fn specifying_more_than_two_assets_should_error() { +fn specifying_more_than_assets_limit_should_error() { TestNet::reset(); ParaA::execute_with(|| { assert_ok!(ParaTokens::deposit(CurrencyId::B, &ALICE, 1_000)); assert_ok!(ParaTokens::deposit(CurrencyId::B1, &ALICE, 1_000)); + assert_ok!(ParaTokens::deposit(CurrencyId::B2, &ALICE, 1_000)); assert_ok!(ParaTokens::deposit(CurrencyId::R, &ALICE, 1_000)); }); ParaB::execute_with(|| { assert_ok!(ParaTokens::deposit(CurrencyId::B, &sibling_a_account(), 1_000)); assert_ok!(ParaTokens::deposit(CurrencyId::B1, &sibling_a_account(), 1_000)); + assert_ok!(ParaTokens::deposit(CurrencyId::B2, &sibling_a_account(), 1_000)); }); Relay::execute_with(|| { @@ -952,7 +1183,12 @@ fn specifying_more_than_two_assets_should_error() { assert_noop!( ParaXTokens::transfer_multicurrencies( Some(ALICE).into(), - vec![(CurrencyId::B, 450), (CurrencyId::B1, 50), (CurrencyId::R, 5000)], + vec![ + (CurrencyId::B, 450), + (CurrencyId::B1, 200), + (CurrencyId::R, 5000), + (CurrencyId::B2, 500) + ], 1, Box::new( ( @@ -973,7 +1209,7 @@ fn specifying_more_than_two_assets_should_error() { } #[test] -fn sending_assets_with_different_reserve_should_fail() { +fn sending_non_fee_assets_with_different_reserve_should_fail() { TestNet::reset(); ParaA::execute_with(|| { @@ -993,7 +1229,7 @@ fn sending_assets_with_different_reserve_should_fail() { assert_noop!( ParaXTokens::transfer_multicurrencies( Some(ALICE).into(), - vec![(CurrencyId::B, 450), (CurrencyId::R, 5000)], + vec![(CurrencyId::B, 450), (CurrencyId::R, 5000), (CurrencyId::A, 450)], 1, Box::new( (