diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/exchange_asset.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/exchange_asset.rs index 800fa967aa475..54775178268c3 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/exchange_asset.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/exchange_asset.rs @@ -13,149 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - assets_balance_on, create_pool_with_wnd_on, foreign_balance_on, - imports::{ - asset_hub_westend_runtime::{ExistentialDeposit, Runtime}, - *, - }, -}; -use asset_hub_westend_runtime::{ - xcm_config::WestendLocation, Balances, ForeignAssets, PolkadotXcm, RuntimeOrigin, -}; -use emulated_integration_tests_common::{accounts::ALICE, xcm_emulator::TestExt}; -use frame_support::{ - assert_err_ignore_postinfo, assert_ok, - traits::fungible::{Inspect, Mutate}, -}; -use parachains_common::{AccountId, Balance}; -use sp_tracing::capture_test_logs; +use crate::{assets_balance_on, create_pool_with_wnd_on, foreign_balance_on, imports::*}; +use emulated_integration_tests_common::xcm_emulator::TestExt; +use frame_support::assert_ok; use std::convert::Into; -use xcm::latest::{Assets, Error as XcmError, Location, Xcm}; - -const UNITS: Balance = 1_000_000_000; - -#[test] -fn exchange_asset_success() { - test_exchange_asset(true, 500 * UNITS, 665 * UNITS, None); -} - -#[test] -fn exchange_asset_insufficient_liquidity() { - let log_capture = capture_test_logs!({ - test_exchange_asset( - true, - 1_000 * UNITS, - 2_000 * UNITS, - Some(InstructionError { index: 1, error: XcmError::NoDeal }), - ); - }); - assert!(log_capture.contains("NoDeal")); -} - -#[test] -fn exchange_asset_insufficient_balance() { - let log_capture = capture_test_logs!({ - test_exchange_asset( - true, - 5_000 * UNITS, - 1_665 * UNITS, - Some(InstructionError { index: 0, error: XcmError::FailedToTransactAsset("") }), - ); - }); - assert!(log_capture.contains("Funds are unavailable")); -} - -#[test] -fn exchange_asset_pool_not_created() { - test_exchange_asset( - false, - 500 * UNITS, - 665 * UNITS, - Some(InstructionError { index: 1, error: XcmError::NoDeal }), - ); -} - -fn test_exchange_asset( - create_pool: bool, - give_amount: Balance, - want_amount: Balance, - expected_error: Option, -) { - let alice: AccountId = Westend::account_id_of(ALICE); - let native_asset_location = WestendLocation::get(); - let native_asset_id = AssetId(native_asset_location.clone()); - let origin = RuntimeOrigin::signed(alice.clone()); - let asset_location = Location::new(1, [Parachain(2001)]); - let asset_id = AssetId(asset_location.clone()); - - // Setup initial state - AssetHubWestend::execute_with(|| { - assert_ok!(>::mint_into( - &alice, - ExistentialDeposit::get() + (1_000 * UNITS) - )); - - assert_ok!(ForeignAssets::force_create( - RuntimeOrigin::root(), - asset_location.clone().into(), - alice.clone().into(), - true, - 1 - )); - }); - - if create_pool { - create_pool_with_wnd_on!(AssetHubWestend, asset_location.clone(), true, alice.clone()); - } - - // Execute and verify swap - AssetHubWestend::execute_with(|| { - let foreign_balance_before = ForeignAssets::balance(asset_location.clone(), &alice); - let wnd_balance_before = Balances::total_balance(&alice); - - let give: Assets = (native_asset_id, give_amount).into(); - let want: Assets = (asset_id, want_amount).into(); - let xcm = Xcm(vec![ - WithdrawAsset(give.clone().into()), - ExchangeAsset { give: give.into(), want: want.into(), maximal: true }, - DepositAsset { assets: Wild(All), beneficiary: alice.clone().into() }, - ]); - - let result = PolkadotXcm::execute(origin, bx!(xcm::VersionedXcm::from(xcm)), Weight::MAX); - - let foreign_balance_after = ForeignAssets::balance(asset_location, &alice); - let wnd_balance_after = Balances::total_balance(&alice); - - if let Some(InstructionError { index, error }) = expected_error { - assert_err_ignore_postinfo!( - result, - pallet_xcm::Error::::LocalExecutionIncompleteWithError { - index, - error: error.into() - } - ); - assert_eq!( - foreign_balance_after, foreign_balance_before, - "Foreign balance changed unexpectedly: got {foreign_balance_after}, expected {foreign_balance_before}" - ); - assert_eq!( - wnd_balance_after, wnd_balance_before, - "WND balance changed unexpectedly: got {wnd_balance_after}, expected {wnd_balance_before}" - ); - } else { - assert_ok!(result); - assert!( - foreign_balance_after >= foreign_balance_before + want_amount, - "Expected foreign balance to increase by at least {want_amount} units, got {foreign_balance_after} from {foreign_balance_before}" - ); - assert_eq!( - wnd_balance_after, wnd_balance_before - give_amount, - "Expected WND balance to decrease by {give_amount} units, got {wnd_balance_after} from {wnd_balance_before}" - ); - } - }); -} +use xcm::latest::{Location, Xcm}; #[test] fn exchange_asset_from_penpal_via_asset_hub_back_to_penpal() { diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs index 818d2986a8fdd..539dc1d7d6c9f 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs @@ -35,8 +35,8 @@ use asset_hub_westend_runtime::{ }; pub use asset_hub_westend_runtime::{AssetConversion, AssetDeposit, CollatorSelection, System}; use asset_test_utils::{ - test_cases_over_bridge::TestBridgingConfig, CollatorSessionKey, CollatorSessionKeys, - ExtBuilder, GovernanceOrigin, SlotDurations, + test_cases::exchange_asset_on_asset_hub_works, test_cases_over_bridge::TestBridgingConfig, + CollatorSessionKey, CollatorSessionKeys, ExtBuilder, GovernanceOrigin, SlotDurations, }; use assets_common::local_and_foreign_assets::ForeignAssetReserveData; use codec::{Decode, Encode}; @@ -66,8 +66,10 @@ use parachains_common::{AccountId, AssetIdForTrustBackedAssets, AuraId, Balance} use sp_consensus_aura::SlotDuration; use sp_core::crypto::Ss58Codec; use sp_runtime::{traits::MaybeEquivalence, Either, MultiAddress}; +use sp_tracing::capture_test_logs; use std::convert::Into; use testnet_parachains_constants::westend::{consensus::*, currency::UNITS}; +use westend_runtime_constants::system_parachain::ASSET_HUB_ID; use xcm::{ latest::{ prelude::{Assets as XcmAssets, *}, @@ -1962,3 +1964,112 @@ fn expensive_erc20_runs_out_of_gas() { .is_err()); }); } + +#[test] +fn exchange_asset_success() { + exchange_asset_on_asset_hub_works::< + Runtime, + RuntimeCall, + RuntimeOrigin, + Block, + ForeignAssetsInstance, + >( + collator_session_keys(), + ASSET_HUB_ID, + AccountId::from(ALICE), + WestendLocation::get(), + true, + 500 * UNITS, + 665 * UNITS, + None, + ); +} + +#[test] +fn exchange_asset_insufficient_liquidity() { + let log_capture = capture_test_logs!({ + exchange_asset_on_asset_hub_works::< + Runtime, + RuntimeCall, + RuntimeOrigin, + Block, + ForeignAssetsInstance, + >( + collator_session_keys(), + ASSET_HUB_ID, + AccountId::from(ALICE), + WestendLocation::get(), + true, + 1_000 * UNITS, + 2_000 * UNITS, + Some(xcm::v5::InstructionError { index: 1, error: xcm::v5::Error::NoDeal }), + ); + }); + assert!(log_capture.contains("NoDeal")); +} + +#[test] +fn exchange_asset_insufficient_balance() { + let log_capture = capture_test_logs!({ + exchange_asset_on_asset_hub_works::< + Runtime, + RuntimeCall, + RuntimeOrigin, + Block, + ForeignAssetsInstance, + >( + collator_session_keys(), + ASSET_HUB_ID, + AccountId::from(ALICE), + WestendLocation::get(), + true, + 5_000 * UNITS, // This amount will be greater than initial balance + 1_665 * UNITS, + Some(xcm::v5::InstructionError { + index: 0, + error: xcm::v5::Error::FailedToTransactAsset(""), + }), + ); + }); + assert!(log_capture.contains("Funds are unavailable")); +} + +#[test] +fn exchange_asset_pool_not_created() { + exchange_asset_on_asset_hub_works::< + Runtime, + RuntimeCall, + RuntimeOrigin, + Block, + ForeignAssetsInstance, + >( + collator_session_keys(), + ASSET_HUB_ID, + AccountId::from(ALICE), + WestendLocation::get(), + false, // Pool not created + 500 * UNITS, + 665 * UNITS, + Some(xcm::v5::InstructionError { index: 1, error: xcm::v5::Error::NoDeal }), + ); +} + +#[test] +fn exchange_asset_from_penpal_via_asset_hub_back_to_penpal() { + exchange_asset_on_asset_hub_works::< + Runtime, + RuntimeCall, + RuntimeOrigin, + Block, + ForeignAssetsInstance, + >( + collator_session_keys(), + ASSET_HUB_ID, + AccountId::from(ALICE), + WestendLocation::get(), + true, + 100_000_000_000u128, + 1_000_000_000u128, + None, + ); +} diff --git a/cumulus/parachains/runtimes/assets/test-utils/src/test_cases.rs b/cumulus/parachains/runtimes/assets/test-utils/src/test_cases.rs index d88aea8a3441f..4cca88103c40f 100644 --- a/cumulus/parachains/runtimes/assets/test-utils/src/test_cases.rs +++ b/cumulus/parachains/runtimes/assets/test-utils/src/test_cases.rs @@ -22,10 +22,11 @@ use codec::Encode; use core::ops::Mul; use cumulus_primitives_core::{UpwardMessageSender, XcmpMessageSource}; use frame_support::{ - assert_noop, assert_ok, + assert_err_ignore_postinfo, assert_noop, assert_ok, traits::{ - fungible::Mutate, fungibles::InspectEnumerable, Currency, Get, OnFinalize, OnInitialize, - OriginTrait, + fungible::Mutate, + fungibles::{InspectEnumerable, Mutate as FungiblesMutate}, + Currency, Get, OnFinalize, OnInitialize, OriginTrait, }, weights::Weight, }; @@ -1927,3 +1928,174 @@ pub fn xcm_payment_api_foreign_asset_pool_works< assert_eq!(execution_fees, expected_weight_foreign_asset_fee); }); } + +pub fn exchange_asset_on_asset_hub_works< + Runtime, + RuntimeCall, + RuntimeOrigin, + Block, + ForeignAssetsPalletInstance, +>( + collator_session_key: CollatorSessionKeys, + runtime_para_id: u32, + account: AccountId, + native_asset_location: Location, + create_pool: bool, + give_amount: Balance, + want_amount: Balance, + expected_error: Option, +) where + Runtime: XcmPaymentApiV2 + + frame_system::Config + + pallet_balances::Config + + pallet_assets::Config< + ForeignAssetsPalletInstance, + AssetId = xcm::v5::Location, + Balance = ::Balance, + > + pallet_asset_conversion::Config< + AssetKind = xcm::v5::Location, + Balance = ::Balance, + > + pallet_session::Config + + pallet_timestamp::Config + + pallet_xcm::Config + + parachain_info::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config, + ValidatorIdOf: From>, + RuntimeOrigin: OriginTrait::AccountId>, + <::Lookup as StaticLookup>::Source: + From<::AccountId>, + Block: BlockT, + ForeignAssetsPalletInstance: 'static, +{ + const UNITS: Balance = 1_000_000_000_000; + + ExtBuilder::::default() + .with_collators(collator_session_key.collators()) + .with_session_keys(collator_session_key.session_keys()) + .with_para_id(runtime_para_id.into()) + .with_tracing() + .build() + .execute_with(|| { + let native_asset_id = xcm::v5::AssetId(native_asset_location.clone()); + let origin = RuntimeOrigin::signed(account.clone()); + let asset_location: Location = Location::new(1, [Parachain(2001)]); + let asset_id = xcm::v5::AssetId(asset_location.clone()); + + let mut total_balance_needed = Balance::from(1_000 * UNITS); + if expected_error.is_none() { + total_balance_needed = total_balance_needed.saturating_add(Balance::from(give_amount)); + } else if give_amount > 1_000_000_000_000u128 * 3 { + total_balance_needed = total_balance_needed + .saturating_add(Balance::from(give_amount).saturating_sub(Balance::from(give_amount / 2))); + } else { + total_balance_needed = total_balance_needed.saturating_add(Balance::from(give_amount)); + } + assert_ok!( as Mutate<_>>::mint_into( + &account, + total_balance_needed + )); + + assert_ok!(pallet_assets::Pallet::::force_create( + RuntimeOrigin::root(), + asset_location.clone().into(), + ::Lookup::unlookup(account.clone()), + true, + 1 + )); + + if create_pool { + assert_ok!(pallet_assets::Pallet::::mint_into( + asset_location.clone(), + &account, + 10_000_000_000_000 + )); + + let native_v5 = xcm::v5::Location::try_from(native_asset_location.clone()) + .expect("conversion works"); + let asset_v5 = xcm::v5::Location::try_from(asset_location.clone()) + .expect("conversion works"); + + assert_ok!(pallet_asset_conversion::Pallet::::create_pool( + RuntimeOrigin::signed(account.clone()), + Box::new(native_v5.clone()), + Box::new(asset_v5.clone()), + )); + assert_ok!(pallet_asset_conversion::Pallet::::add_liquidity( + RuntimeOrigin::signed(account.clone()), + Box::new(native_v5.clone()), + Box::new(asset_v5.clone()), + 1_000_000_000_000, + 2_000_000_000_000, + 0, + 0, + account.clone(), + )); + } + + let foreign_balance_before = pallet_assets::Pallet::::balance(asset_location.clone().into(), &account); + let native_balance_before = pallet_balances::Pallet::::total_balance(&account); + + let want_amount_min = if create_pool && expected_error.is_none() { + let native_v5 = xcm::v5::Location::try_from(native_asset_location.clone()) + .expect("conversion works"); + let asset_v5 = xcm::v5::Location::try_from(asset_location.clone()) + .expect("conversion works"); + pallet_asset_conversion::Pallet::::quote_price_exact_tokens_for_tokens( + native_v5, + asset_v5, + give_amount, + true, + ) + .map(|quoted| (quoted * 90) / 100) + .unwrap_or(want_amount) + } else { + want_amount + }; + + let give: Assets = (native_asset_id, give_amount).into(); + let want: Assets = (asset_id, want_amount_min).into(); + let xcm = Xcm(vec![ + WithdrawAsset(give.clone().into()), + ExchangeAsset { give: give.into(), want: want.into(), maximal: true }, + DepositAsset { assets: Wild(All), beneficiary: account.clone().into() }, + ]); + + let result = pallet_xcm::Pallet::::execute( + origin, + xcm::VersionedXcm::from(xcm).into(), + Weight::MAX + ); + + let foreign_balance_after = pallet_assets::Pallet::::balance(asset_location.into(), &account); + let native_balance_after = pallet_balances::Pallet::::total_balance(&account); + + if let Some(xcm::v5::InstructionError { index, error }) = expected_error { + assert_err_ignore_postinfo!( + result, + pallet_xcm::Error::::LocalExecutionIncompleteWithError { + index, + error: error.into() + } + ); + assert_eq!( + foreign_balance_after, foreign_balance_before, + "Foreign balance changed unexpectedly: got {foreign_balance_after}, expected {foreign_balance_before}" + ); + assert_eq!( + native_balance_after, native_balance_before, + "Native balance changed unexpectedly: got {native_balance_after}, expected {native_balance_before}" + ); + } else { + assert_ok!(result); + assert!( + foreign_balance_after >= foreign_balance_before + want_amount_min, + "Expected foreign balance to increase by at least {want_amount_min} units, got {foreign_balance_after} from {foreign_balance_before}" + ); + assert_eq!( + native_balance_after, native_balance_before - give_amount, + "Expected WND balance to decrease by {give_amount} units, got {native_balance_after} from {native_balance_before}" + ); + } + }); +} diff --git a/prdoc/pr_10721.prdoc b/prdoc/pr_10721.prdoc new file mode 100644 index 0000000000000..883cbc9da772a --- /dev/null +++ b/prdoc/pr_10721.prdoc @@ -0,0 +1,12 @@ +title: Integrate asset test utilities for asset hub westend +doc: +- audience: Runtime Dev + description: |- + The PR migrates exchange_asset tests from integration tests to unit tests in the AssetHubWestend runtime and introduces a shared helper to reduce duplication. +crates: +- name: asset-hub-westend-integration-tests + bump: none +- name: asset-hub-westend-runtime + bump: patch +- name: asset-test-utils + bump: minor