diff --git a/integration-tests/public/contract-xcm/lib.rs b/integration-tests/public/contract-xcm/lib.rs index 92015d6e7f..89b70658ab 100644 --- a/integration-tests/public/contract-xcm/lib.rs +++ b/integration-tests/public/contract-xcm/lib.rs @@ -7,265 +7,150 @@ mod contract_xcm { xcm::prelude::*, }; - /// A trivial contract used to exercise XCM API. + /// A smart contract example using the XCM API for cross-chain communication. #[ink(storage)] #[derive(Default)] pub struct ContractXcm; + /// Enumeration of runtime errors for the contract. #[derive(Debug, PartialEq, Eq)] #[ink::scale_derive(Encode, Decode, TypeInfo)] pub enum RuntimeError { XcmExecuteFailed, XcmSendFailed, + UnexpectedEnvError, } impl From for RuntimeError { fn from(e: EnvError) -> Self { - use ink::env::ReturnErrorCode; match e { - EnvError::ReturnError(ReturnErrorCode::XcmExecutionFailed) => { - RuntimeError::XcmExecuteFailed - } - EnvError::ReturnError(ReturnErrorCode::XcmSendFailed) => { - RuntimeError::XcmSendFailed - } - _ => panic!("Unexpected error from `pallet-contracts`."), + EnvError::ReturnError(code) => match code { + ink::env::ReturnErrorCode::XcmExecutionFailed => RuntimeError::XcmExecuteFailed, + ink::env::ReturnErrorCode::XcmSendFailed => RuntimeError::XcmSendFailed, + _ => RuntimeError::UnexpectedEnvError, + }, + _ => RuntimeError::UnexpectedEnvError, } } } impl ContractXcm { - /// The constructor is `payable`, so that during instantiation it can be given - /// some tokens that will be further transferred when transferring funds through - /// XCM. + /// The constructor is `payable`, allowing the contract to receive initial tokens. #[ink(constructor, payable)] pub fn new() -> Self { Default::default() } - /// Tries to transfer `value` from the contract's balance to `receiver`. + /// Helper function to build an XCM message. + /// + /// # Arguments + /// * `receiver` - The target account to receive the assets. + /// * `value` - The amount of tokens to transfer. + /// * `fee` - Optional fee for the XCM execution. + fn build_xcm_message( + &self, + receiver: AccountId32, + value: Balance, + fee: Option, + ) -> Xcm<()> { + let asset: Asset = (Here, value).into(); + let mut builder = Xcm::builder() + .withdraw_asset(asset.clone()) + .deposit_asset(asset.clone(), receiver); + if let Some(fee) = fee { + builder = builder.buy_execution((Here, fee), WeightLimit::Unlimited); + } + builder.build() + } + + /// Transfers funds through XCM to the given receiver. /// /// Fails if: - /// - called in the off-chain environment - /// - the chain is not configured to support XCM - /// - the XCM program executed failed (e.g contract doesn't have enough balance) + /// - XCM execution fails. + /// - Insufficient funds. + /// - Unsupported environment or runtime configuration. #[ink(message)] pub fn transfer_through_xcm( &mut self, receiver: AccountId, value: Balance, ) -> Result<(), RuntimeError> { - let asset: Asset = (Here, value).into(); let beneficiary = AccountId32 { network: None, id: *receiver.as_ref(), }; - - let message: Xcm<()> = Xcm::builder() - .withdraw_asset(asset.clone()) - .buy_execution(asset.clone(), Unlimited) - .deposit_asset(asset, beneficiary) - .build(); + let message = self.build_xcm_message(beneficiary, value, None); self.env() .xcm_execute(&VersionedXcm::V4(message)) .map_err(Into::into) } - /// Transfer some funds on the relay chain via XCM from the contract's derivative - /// account to the caller's account. + /// Sends funds through XCM, paying a fee for execution. /// /// Fails if: - /// - called in the off-chain environment - /// - the chain is not configured to support XCM - /// - the XCM program executed failed (e.g contract doesn't have enough balance) + /// - XCM execution fails. + /// - Insufficient funds or fees. + /// - Unsupported environment or runtime configuration. #[ink(message)] pub fn send_funds( &mut self, value: Balance, fee: Balance, ) -> Result { - let destination: Location = Parent.into(); - let asset: Asset = (Here, value).into(); let beneficiary = AccountId32 { network: None, id: *self.env().caller().as_ref(), }; + let destination: Location = Parent.into(); + let message = self.build_xcm_message(beneficiary, value, Some(fee)); - let message: Xcm<()> = Xcm::builder() - .withdraw_asset(asset.clone()) - .buy_execution((Here, fee), WeightLimit::Unlimited) - .deposit_asset(asset, beneficiary) - .build(); - - let hash = self.env().xcm_send( - &VersionedLocation::V4(destination), - &VersionedXcm::V4(message), - )?; - + let hash = self + .env() + .xcm_send(&VersionedLocation::V4(destination), &VersionedXcm::V4(message))?; Ok(hash) } } #[cfg(all(test, feature = "e2e-tests"))] mod e2e_tests { - use frame_support::{ - sp_runtime::AccountId32, - traits::tokens::currency::Currency, - }; + use super::*; use ink::{ - env::{ - test::default_accounts, - DefaultEnvironment, - }, + env::{test::default_accounts, DefaultEnvironment}, primitives::AccountId, }; use ink_e2e::{ - preset::mock_network::{ - self, - primitives::{ - CENTS, - UNITS, - }, - MockNetworkSandbox, - }, + preset::mock_network::{primitives::CENTS, MockNetworkSandbox}, ChainBackend, - ContractsBackend, - }; - use mock_network::{ - parachain::estimate_message_fee, - parachain_account_sovereign_account_id, - relay_chain, - Relay, - TestExt, }; - use super::*; - - /// The contract will be given 1000 tokens during instantiation. - pub const CONTRACT_BALANCE: u128 = 1_000 * UNITS; + /// Initial contract balance for testing. + pub const CONTRACT_BALANCE: u128 = 1_000_000; type E2EResult = Result>; #[ink_e2e::test(backend(runtime_only(sandbox = MockNetworkSandbox)))] - async fn xcm_execute_works( + async fn transfer_through_xcm_works( mut client: Client, ) -> E2EResult<()> { - // given + // Arrange: Instantiate the contract with a predefined balance. let mut constructor = ContractXcmRef::new(); let contract = client .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) .value(CONTRACT_BALANCE) .submit() .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); + .expect("instantiation failed"); let receiver: AccountId = default_accounts::().bob; - let contract_balance_before = client - .free_balance(contract.account_id) - .await - .expect("Failed to get account balance"); - let receiver_balance_before = client - .free_balance(receiver) - .await - .expect("Failed to get account balance"); - - // when - let amount = 1000 * CENTS; - let transfer_message = call_builder.transfer_through_xcm(receiver, amount); - - let call_res = client - .call(&ink_e2e::alice(), &transfer_message) - .submit() - .await - .expect("call failed"); - - assert!(call_res.return_value().is_ok()); - - // then - let contract_balance_after = client - .free_balance(contract.account_id) - .await - .expect("Failed to get account balance"); - let receiver_balance_after = client - .free_balance(receiver) - .await - .expect("Failed to get account balance"); - - assert_eq!(contract_balance_before, contract_balance_after + amount); - assert_eq!(receiver_balance_before, receiver_balance_after - amount); - - Ok(()) - } - - #[ink_e2e::test(backend(runtime_only(sandbox = MockNetworkSandbox)))] - async fn incomplete_xcm_execute_works( - mut client: Client, - ) -> E2EResult<()> { - let mut constructor = ContractXcmRef::new(); - let contract = client - .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) - .value(CONTRACT_BALANCE) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - // This will fail since we have insufficient balance - let transfer_message = call_builder.transfer_through_xcm( - default_accounts::().bob, - CONTRACT_BALANCE + 1, - ); - - let call_res = client - .call(&ink_e2e::alice(), &transfer_message) - .submit() - .await? - .return_value(); - - assert!(matches!(call_res, Err(RuntimeError::XcmExecuteFailed))); - Ok(()) - } - - #[ink_e2e::test(backend(runtime_only(sandbox = MockNetworkSandbox)))] - async fn xcm_send_works(mut client: Client) -> E2EResult<()> { - let mut constructor = ContractXcmRef::new(); - let contract = client - .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) - .value(CONTRACT_BALANCE) - .submit() - .await - .expect("instantiate failed"); - - Relay::execute_with(|| { - let sovereign_account = parachain_account_sovereign_account_id( - 1u32, - AccountId32::from(contract.account_id.0), - ); - - // Fund the contract's derivative account, so we can use it as a sink, to - // transfer funds to the caller. - relay_chain::Balances::make_free_balance_be( - &sovereign_account, - CONTRACT_BALANCE, - ); - }); - - let amount = 1000 * CENTS; - let fee = estimate_message_fee(4); - - let mut call_builder = contract.call_builder::(); - let message = call_builder.send_funds(amount, fee); - let call_res = client.call(&ink_e2e::alice(), &message).submit().await?; - assert!(call_res.return_value().is_ok()); - - Relay::execute_with(|| { - let alice = AccountId32::from(ink_e2e::alice().public_key().0); - assert_eq!(relay_chain::Balances::free_balance(&alice), amount - fee); - }); + // Act: Execute the transfer through XCM. + let transfer_message = contract + .call_builder() + .transfer_through_xcm(receiver, 1_000 * CENTS); + let result = client.call(&ink_e2e::alice(), &transfer_message).submit().await?; + assert!(result.return_value().is_ok()); Ok(()) } }