From d5053ac4161b6e3f634a3ffb6df07637058e9f55 Mon Sep 17 00:00:00 2001
From: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Date: Wed, 29 May 2024 20:57:17 +0100
Subject: [PATCH] Change `XcmDryRunApi::dry_run_extrinsic` to take a call
 instead (#4621)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Follow-up to the new `XcmDryRunApi` runtime API introduced in
https://github.com/paritytech/polkadot-sdk/pull/3872.

Taking an extrinsic means the frontend has to sign first to dry-run and
once again to submit.
This is bad UX which is solved by taking an `origin` and a `call`.
This also has the benefit of being able to dry-run as any account, since
it needs no signature.

This is a breaking change since I changed `dry_run_extrinsic` to
`dry_run_call`, however, this API is still only on testnets.
The crates are bumped accordingly.

As a part of this PR, I changed the name of the API from `XcmDryRunApi`
to just `DryRunApi`, since it can be used for general dry-running :)

Step towards https://github.com/paritytech/polkadot-sdk/issues/690.

Example of calling the API with PAPI, not the best code, just testing :)

```ts
// We just build a call, the arguments make it look very big though.
const call = localApi.tx.XcmPallet.transfer_assets({
  dest: XcmVersionedLocation.V4({ parents: 0, interior: XcmV4Junctions.X1(XcmV4Junction.Parachain(1000)) }),
  beneficiary: XcmVersionedLocation.V4({ parents: 0, interior: XcmV4Junctions.X1(XcmV4Junction.AccountId32({ network: undefined, id: Binary.fromBytes(encodeAccount(account.address)) })) }),
  weight_limit: XcmV3WeightLimit.Unlimited(),
  assets: XcmVersionedAssets.V4([{
    id: { parents: 0, interior: XcmV4Junctions.Here() },
    fun: XcmV3MultiassetFungibility.Fungible(1_000_000_000_000n) }
  ]),
  fee_asset_item: 0,
});
// We call the API passing in a signed origin
const result = await localApi.apis.XcmDryRunApi.dry_run_call(
  WestendRuntimeOriginCaller.system(DispatchRawOrigin.Signed(account.address)),
  call.decodedCall
);
if (result.success && result.value.execution_result.success) {
  // We find the forwarded XCM we want. The first one going to AssetHub in this case.
  const xcmsToAssetHub = result.value.forwarded_xcms.find(([location, _]) => (
    location.type === "V4" &&
      location.value.parents === 0 &&
      location.value.interior.type === "X1"
      && location.value.interior.value.type === "Parachain"
      && location.value.interior.value.value === 1000
  ))!;

  // We can even find the delivery fees for that forwarded XCM.
  const deliveryFeesQuery = await localApi.apis.XcmPaymentApi.query_delivery_fees(xcmsToAssetHub[0], xcmsToAssetHub[1][0]);

  if (deliveryFeesQuery.success) {
    const amount = deliveryFeesQuery.value.type === "V4" && deliveryFeesQuery.value.value[0].fun.type === "Fungible" && deliveryFeesQuery.value.value[0].fun.value.valueOf() || 0n;
    // We store them in state somewhere.
    setDeliveryFees(formatAmount(BigInt(amount)));
  }
}
```

---------

Co-authored-by: Bastian Köcher <git@kchr.de>
---
 .../src/tests/xcm_fee_estimation.rs           | 86 ++--------------
 .../assets/asset-hub-rococo/src/lib.rs        | 66 ++-----------
 .../assets/asset-hub-westend/src/lib.rs       | 66 ++-----------
 .../runtimes/testing/penpal/src/lib.rs        | 27 +++---
 cumulus/xcm/xcm-emulator/src/lib.rs           |  3 +
 polkadot/node/service/src/fake_runtime_api.rs |  4 +-
 polkadot/runtime/rococo/src/lib.rs            | 60 +-----------
 polkadot/runtime/westend/src/lib.rs           | 60 +-----------
 polkadot/xcm/pallet-xcm/src/lib.rs            | 97 +++++++++++++++++--
 .../src/dry_run.rs                            | 19 ++--
 .../tests/fee_estimation.rs                   | 86 ++++++----------
 .../xcm-fee-payment-runtime-api/tests/mock.rs | 57 +++--------
 prdoc/pr_4621.prdoc                           | 43 ++++++++
 13 files changed, 237 insertions(+), 437 deletions(-)
 create mode 100644 prdoc/pr_4621.prdoc

diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs
index 3e311ef95652e..dc89ef1f7a44e 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs
@@ -17,16 +17,15 @@
 
 use crate::imports::*;
 
-use sp_keyring::AccountKeyring::Alice;
-use sp_runtime::{generic, MultiSignature};
+use frame_system::RawOrigin;
 use xcm_fee_payment_runtime_api::{
-	dry_run::runtime_decl_for_xcm_dry_run_api::XcmDryRunApiV1,
+	dry_run::runtime_decl_for_dry_run_api::DryRunApiV1,
 	fees::runtime_decl_for_xcm_payment_api::XcmPaymentApiV1,
 };
 
 /// We are able to dry-run and estimate the fees for a teleport between relay and system para.
 /// Scenario: Alice on Westend relay chain wants to teleport WND to Asset Hub.
-/// We want to know the fees using the `XcmDryRunApi` and `XcmPaymentApi`.
+/// We want to know the fees using the `DryRunApi` and `XcmPaymentApi`.
 #[test]
 fn teleport_relay_system_para_works() {
 	let destination: Location = Parachain(1000).into(); // Asset Hub.
@@ -42,6 +41,7 @@ fn teleport_relay_system_para_works() {
 	<Westend as TestExt>::new_ext().execute_with(|| {
 		type Runtime = <Westend as Chain>::Runtime;
 		type RuntimeCall = <Westend as Chain>::RuntimeCall;
+		type OriginCaller = <Westend as Chain>::OriginCaller;
 
 		let call = RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets {
 			dest: Box::new(VersionedLocation::V4(destination.clone())),
@@ -50,9 +50,8 @@ fn teleport_relay_system_para_works() {
 			fee_asset_item: 0,
 			weight_limit: Unlimited,
 		});
-		let sender = Alice; // Is the same as `WestendSender`.
-		let extrinsic = construct_extrinsic_westend(sender, call);
-		let result = Runtime::dry_run_extrinsic(extrinsic).unwrap();
+		let origin = OriginCaller::system(RawOrigin::Signed(WestendSender::get()));
+		let result = Runtime::dry_run_call(origin, call).unwrap();
 		assert_eq!(result.forwarded_xcms.len(), 1);
 		let (destination_to_query, messages_to_query) = &result.forwarded_xcms[0];
 		assert_eq!(messages_to_query.len(), 1);
@@ -105,7 +104,7 @@ fn teleport_relay_system_para_works() {
 
 /// We are able to dry-run and estimate the fees for a multi-hop XCM journey.
 /// Scenario: Alice on PenpalA has some WND and wants to send them to PenpalB.
-/// We want to know the fees using the `XcmDryRunApi` and `XcmPaymentApi`.
+/// We want to know the fees using the `DryRunApi` and `XcmPaymentApi`.
 #[test]
 fn multi_hop_works() {
 	let destination = PenpalA::sibling_location_of(PenpalB::para_id());
@@ -142,6 +141,7 @@ fn multi_hop_works() {
 	<PenpalA as TestExt>::execute_with(|| {
 		type Runtime = <PenpalA as Chain>::Runtime;
 		type RuntimeCall = <PenpalA as Chain>::RuntimeCall;
+		type OriginCaller = <PenpalA as Chain>::OriginCaller;
 
 		let call = RuntimeCall::PolkadotXcm(pallet_xcm::Call::transfer_assets {
 			dest: Box::new(VersionedLocation::V4(destination.clone())),
@@ -150,9 +150,8 @@ fn multi_hop_works() {
 			fee_asset_item: 0,
 			weight_limit: Unlimited,
 		});
-		let sender = Alice; // Same as `PenpalASender`.
-		let extrinsic = construct_extrinsic_penpal(sender, call);
-		let result = Runtime::dry_run_extrinsic(extrinsic).unwrap();
+		let origin = OriginCaller::system(RawOrigin::Signed(PenpalASender::get()));
+		let result = Runtime::dry_run_call(origin, call).unwrap();
 		assert_eq!(result.forwarded_xcms.len(), 1);
 		let (destination_to_query, messages_to_query) = &result.forwarded_xcms[0];
 		assert_eq!(messages_to_query.len(), 1);
@@ -304,68 +303,3 @@ fn transfer_assets_para_to_para(test: ParaToParaThroughRelayTest) -> DispatchRes
 		test.args.weight_limit,
 	)
 }
-
-// Constructs the SignedExtra component of an extrinsic for the Westend runtime.
-fn construct_extrinsic_westend(
-	sender: sp_keyring::AccountKeyring,
-	call: westend_runtime::RuntimeCall,
-) -> westend_runtime::UncheckedExtrinsic {
-	type Runtime = <Westend as Chain>::Runtime;
-	let account_id = <Runtime as frame_system::Config>::AccountId::from(sender.public());
-	let tip = 0;
-	let extra: westend_runtime::SignedExtra = (
-		frame_system::CheckNonZeroSender::<Runtime>::new(),
-		frame_system::CheckSpecVersion::<Runtime>::new(),
-		frame_system::CheckTxVersion::<Runtime>::new(),
-		frame_system::CheckGenesis::<Runtime>::new(),
-		frame_system::CheckMortality::<Runtime>::from(sp_runtime::generic::Era::immortal()),
-		frame_system::CheckNonce::<Runtime>::from(
-			frame_system::Pallet::<Runtime>::account(&account_id).nonce,
-		),
-		frame_system::CheckWeight::<Runtime>::new(),
-		pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip),
-		frame_metadata_hash_extension::CheckMetadataHash::<Runtime>::new(false),
-	);
-	let raw_payload = westend_runtime::SignedPayload::new(call, extra).unwrap();
-	let signature = raw_payload.using_encoded(|payload| sender.sign(payload));
-	let (call, extra, _) = raw_payload.deconstruct();
-	westend_runtime::UncheckedExtrinsic::new_signed(
-		call,
-		account_id.into(),
-		MultiSignature::Sr25519(signature),
-		extra,
-	)
-}
-
-// Constructs the SignedExtra component of an extrinsic for the Westend runtime.
-fn construct_extrinsic_penpal(
-	sender: sp_keyring::AccountKeyring,
-	call: penpal_runtime::RuntimeCall,
-) -> penpal_runtime::UncheckedExtrinsic {
-	type Runtime = <PenpalA as Chain>::Runtime;
-	let account_id = <Runtime as frame_system::Config>::AccountId::from(sender.public());
-	let tip = 0;
-	let extra: penpal_runtime::SignedExtra = (
-		frame_system::CheckNonZeroSender::<Runtime>::new(),
-		frame_system::CheckSpecVersion::<Runtime>::new(),
-		frame_system::CheckTxVersion::<Runtime>::new(),
-		frame_system::CheckGenesis::<Runtime>::new(),
-		frame_system::CheckEra::<Runtime>::from(generic::Era::immortal()),
-		frame_system::CheckNonce::<Runtime>::from(
-			frame_system::Pallet::<Runtime>::account(&account_id).nonce,
-		),
-		frame_system::CheckWeight::<Runtime>::new(),
-		pallet_asset_tx_payment::ChargeAssetTxPayment::<Runtime>::from(tip, None),
-	);
-	type SignedPayload =
-		generic::SignedPayload<penpal_runtime::RuntimeCall, penpal_runtime::SignedExtra>;
-	let raw_payload = SignedPayload::new(call, extra).unwrap();
-	let signature = raw_payload.using_encoded(|payload| sender.sign(payload));
-	let (call, extra, _) = raw_payload.deconstruct();
-	penpal_runtime::UncheckedExtrinsic::new_signed(
-		call,
-		account_id.into(),
-		MultiSignature::Sr25519(signature),
-		extra,
-	)
-}
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
index 4705d12e60c8f..e3a106c6ab9a6 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
@@ -101,7 +101,7 @@ use xcm::{
 	IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm,
 };
 use xcm_fee_payment_runtime_api::{
-	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	dry_run::{CallDryRunEffects, Error as XcmDryRunApiError, XcmDryRunEffects},
 	fees::Error as XcmPaymentApiError,
 };
 
@@ -1332,67 +1332,13 @@ impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
-		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
-			use xcm_builder::InspectMessageQueues;
-			use xcm_executor::RecordXcm;
-			use xcm::prelude::*;
-
-			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
-			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
-					"Applying extrinsic failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::InvalidExtrinsic
-			})?;
-			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
-			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
-			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
-			Ok(ExtrinsicDryRunEffects {
-				local_xcm: local_xcm.map(VersionedXcm::<()>::from),
-				forwarded_xcms,
-				emitted_events: events,
-				execution_result: result,
-			})
+	impl xcm_fee_payment_runtime_api::dry_run::DryRunApi<Block, RuntimeCall, RuntimeEvent, OriginCaller> for Runtime {
+		fn dry_run_call(origin: OriginCaller, call: RuntimeCall) -> Result<CallDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			PolkadotXcm::dry_run_call::<Runtime, xcm_config::XcmRouter, OriginCaller, RuntimeCall>(origin, call)
 		}
 
-		fn dry_run_xcm(origin_location: VersionedLocation, program: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
-			use xcm_builder::InspectMessageQueues;
-			use xcm::prelude::*;
-
-			let origin_location: Location = origin_location.try_into().map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
-					"Location version conversion failed with error: {:?}",
-					error,
-				);
-				XcmDryRunApiError::VersionedConversionFailed
-			})?;
-			let program: Xcm<RuntimeCall> = program.try_into().map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
-					"Xcm version conversion failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::VersionedConversionFailed
-			})?;
-			let mut hash = program.using_encoded(sp_core::hashing::blake2_256);
-			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
-				origin_location,
-				program,
-				&mut hash,
-				Weight::MAX, // Max limit available for execution.
-				Weight::zero(),
-			);
-			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
-			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
-			Ok(XcmDryRunEffects {
-				forwarded_xcms,
-				emitted_events: events,
-				execution_result: result,
-			})
+		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			PolkadotXcm::dry_run_xcm::<Runtime, xcm_config::XcmRouter, RuntimeCall, xcm_config::XcmConfig>(origin_location, xcm)
 		}
 	}
 
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
index a82094d6f8a65..ececae3ef0a77 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
@@ -100,7 +100,7 @@ use xcm::latest::prelude::{
 };
 
 use xcm_fee_payment_runtime_api::{
-	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	dry_run::{CallDryRunEffects, Error as XcmDryRunApiError, XcmDryRunEffects},
 	fees::Error as XcmPaymentApiError,
 };
 
@@ -1368,67 +1368,13 @@ impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
-		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
-			use xcm_builder::InspectMessageQueues;
-			use xcm_executor::RecordXcm;
-			use xcm::prelude::*;
-
-			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
-			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
-					"Applying extrinsic failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::InvalidExtrinsic
-			})?;
-			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
-			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
-			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
-			Ok(ExtrinsicDryRunEffects {
-				local_xcm: local_xcm.map(VersionedXcm::<()>::from),
-				forwarded_xcms,
-				emitted_events: events,
-				execution_result: result,
-			})
+	impl xcm_fee_payment_runtime_api::dry_run::DryRunApi<Block, RuntimeCall, RuntimeEvent, OriginCaller> for Runtime {
+		fn dry_run_call(origin: OriginCaller, call: RuntimeCall) -> Result<CallDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			PolkadotXcm::dry_run_call::<Runtime, xcm_config::XcmRouter, OriginCaller, RuntimeCall>(origin, call)
 		}
 
-		fn dry_run_xcm(origin_location: VersionedLocation, program: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
-			use xcm_builder::InspectMessageQueues;
-			use xcm::prelude::*;
-
-			let origin_location: Location = origin_location.try_into().map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
-					"Location version conversion failed with error: {:?}",
-					error,
-				);
-				XcmDryRunApiError::VersionedConversionFailed
-			})?;
-			let program: Xcm<RuntimeCall> = program.try_into().map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
-					"Xcm version conversion failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::VersionedConversionFailed
-			})?;
-			let mut hash = program.using_encoded(sp_core::hashing::blake2_256);
-			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
-				origin_location,
-				program,
-				&mut hash,
-				Weight::MAX, // Max limit available for execution.
-				Weight::zero(),
-			);
-			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
-			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
-			Ok(XcmDryRunEffects {
-				forwarded_xcms,
-				emitted_events: events,
-				execution_result: result,
-			})
+		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			PolkadotXcm::dry_run_xcm::<Runtime, xcm_config::XcmRouter, RuntimeCall, xcm_config::XcmConfig>(origin_location, xcm)
 		}
 	}
 
diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs
index 8afe56cddefab..7e4a013117bf6 100644
--- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs
+++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs
@@ -64,7 +64,7 @@ pub use sp_consensus_aura::sr25519::AuthorityId as AuraId;
 use sp_core::{crypto::KeyTypeId, OpaqueMetadata};
 use sp_runtime::{
 	create_runtime_str, generic, impl_opaque_keys,
-	traits::{AccountIdLookup, BlakeTwo256, Block as BlockT},
+	traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, Dispatchable},
 	transaction_validity::{TransactionSource, TransactionValidity},
 	ApplyExtrinsicResult,
 };
@@ -86,7 +86,7 @@ use xcm::{
 	IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm,
 };
 use xcm_fee_payment_runtime_api::{
-	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	dry_run::{CallDryRunEffects, Error as XcmDryRunApiError, XcmDryRunEffects},
 	fees::Error as XcmPaymentApiError,
 };
 
@@ -886,25 +886,19 @@ impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
-		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+	impl xcm_fee_payment_runtime_api::dry_run::DryRunApi<Block, RuntimeCall, RuntimeEvent, OriginCaller> for Runtime {
+		fn dry_run_call(origin: OriginCaller, call: RuntimeCall) -> Result<CallDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
 			use xcm_builder::InspectMessageQueues;
 			use xcm_executor::RecordXcm;
 			use xcm::prelude::*;
-
 			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
-			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
-					"Applying extrinsic failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::InvalidExtrinsic
-			})?;
+			frame_system::Pallet::<Runtime>::reset_events(); // To make sure we only record events from current call.
+			let result = call.dispatch(origin.into());
+			pallet_xcm::Pallet::<Runtime>::set_record_xcm(false);
 			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
 			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
 			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
-			Ok(ExtrinsicDryRunEffects {
+			Ok(CallDryRunEffects {
 				local_xcm: local_xcm.map(VersionedXcm::<()>::from),
 				forwarded_xcms,
 				emitted_events: events,
@@ -918,7 +912,7 @@ impl_runtime_apis! {
 
 			let origin_location: Location = origin_location.try_into().map_err(|error| {
 				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					target: "xcm::DryRunApi::dry_run_xcm",
 					"Location version conversion failed with error: {:?}",
 					error,
 				);
@@ -926,13 +920,14 @@ impl_runtime_apis! {
 			})?;
 			let program: Xcm<RuntimeCall> = program.try_into().map_err(|error| {
 				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					target: "xcm::DryRunApi::dry_run_xcm",
 					"Xcm version conversion failed with error {:?}",
 					error,
 				);
 				XcmDryRunApiError::VersionedConversionFailed
 			})?;
 			let mut hash = program.using_encoded(sp_core::hashing::blake2_256);
+			frame_system::Pallet::<Runtime>::reset_events(); // To make sure we only record events from current call.
 			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
 				origin_location,
 				program,
diff --git a/cumulus/xcm/xcm-emulator/src/lib.rs b/cumulus/xcm/xcm-emulator/src/lib.rs
index a50f33951d056..1a3f3930cb347 100644
--- a/cumulus/xcm/xcm-emulator/src/lib.rs
+++ b/cumulus/xcm/xcm-emulator/src/lib.rs
@@ -215,6 +215,7 @@ pub trait Chain: TestExt {
 	type RuntimeOrigin;
 	type RuntimeEvent;
 	type System;
+	type OriginCaller;
 
 	fn account_id_of(seed: &str) -> AccountId {
 		helpers::get_account_id_from_seed::<sr25519::Public>(seed)
@@ -366,6 +367,7 @@ macro_rules! decl_test_relay_chains {
 				type RuntimeOrigin = $runtime::RuntimeOrigin;
 				type RuntimeEvent = $runtime::RuntimeEvent;
 				type System = $crate::SystemPallet::<Self::Runtime>;
+				type OriginCaller = $runtime::OriginCaller;
 
 				fn account_data_of(account: $crate::AccountIdOf<Self::Runtime>) -> $crate::AccountData<$crate::Balance> {
 					<Self as $crate::TestExt>::ext_wrapper(|| $crate::SystemPallet::<Self::Runtime>::account(account).data.into())
@@ -600,6 +602,7 @@ macro_rules! decl_test_parachains {
 				type RuntimeOrigin = $runtime::RuntimeOrigin;
 				type RuntimeEvent = $runtime::RuntimeEvent;
 				type System = $crate::SystemPallet::<Self::Runtime>;
+				type OriginCaller = $runtime::OriginCaller;
 				type Network = N;
 
 				fn account_data_of(account: $crate::AccountIdOf<Self::Runtime>) -> $crate::AccountData<$crate::Balance> {
diff --git a/polkadot/node/service/src/fake_runtime_api.rs b/polkadot/node/service/src/fake_runtime_api.rs
index 03c4836020d98..34abc76813ffd 100644
--- a/polkadot/node/service/src/fake_runtime_api.rs
+++ b/polkadot/node/service/src/fake_runtime_api.rs
@@ -416,8 +416,8 @@ sp_api::impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, (), ()> for Runtime {
-		fn dry_run_extrinsic(_: <Block as BlockT>::Extrinsic) -> Result<xcm_fee_payment_runtime_api::dry_run::ExtrinsicDryRunEffects<()>, xcm_fee_payment_runtime_api::dry_run::Error> {
+	impl xcm_fee_payment_runtime_api::dry_run::DryRunApi<Block, (), (), ()> for Runtime {
+		fn dry_run_call(_: (), _: ()) -> Result<xcm_fee_payment_runtime_api::dry_run::CallDryRunEffects<()>, xcm_fee_payment_runtime_api::dry_run::Error> {
 			unimplemented!()
 		}
 
diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs
index f0cc7e046f29c..c2614f7e96e88 100644
--- a/polkadot/runtime/rococo/src/lib.rs
+++ b/polkadot/runtime/rococo/src/lib.rs
@@ -135,7 +135,7 @@ use governance::{
 	TreasurySpender,
 };
 use xcm_fee_payment_runtime_api::{
-	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	dry_run::{CallDryRunEffects, Error as XcmDryRunApiError, XcmDryRunEffects},
 	fees::Error as XcmPaymentApiError,
 };
 
@@ -1809,63 +1809,13 @@ sp_api::impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
-		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
-			use xcm_builder::InspectMessageQueues;
-			use xcm_executor::RecordXcm;
-			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
-			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
-					"Applying extrinsic failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::InvalidExtrinsic
-			})?;
-			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
-			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
-			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
-			Ok(ExtrinsicDryRunEffects {
-				local_xcm: local_xcm.map(VersionedXcm::<()>::from),
-				forwarded_xcms,
-				emitted_events: events,
-				execution_result: result,
-			})
+	impl xcm_fee_payment_runtime_api::dry_run::DryRunApi<Block, RuntimeCall, RuntimeEvent, OriginCaller> for Runtime {
+		fn dry_run_call(origin: OriginCaller, call: RuntimeCall) -> Result<CallDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			XcmPallet::dry_run_call::<Runtime, xcm_config::XcmRouter, OriginCaller, RuntimeCall>(origin, call)
 		}
 
 		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
-			use xcm_builder::InspectMessageQueues;
-			let origin_location: Location = origin_location.try_into().map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
-					"Location version conversion failed with error: {:?}",
-					error,
-				);
-				XcmDryRunApiError::VersionedConversionFailed
-			})?;
-			let xcm: Xcm<RuntimeCall> = xcm.try_into().map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
-					"Xcm version conversion failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::VersionedConversionFailed
-			})?;
-			let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256);
-			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
-				origin_location,
-				xcm,
-				&mut hash,
-				Weight::MAX, // Max limit available for execution.
-				Weight::zero(),
-			);
-			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
-			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
-			Ok(XcmDryRunEffects {
-				forwarded_xcms,
-				emitted_events: events,
-				execution_result: result,
-			})
+			XcmPallet::dry_run_xcm::<Runtime, xcm_config::XcmRouter, RuntimeCall, xcm_config::XcmConfig>(origin_location, xcm)
 		}
 	}
 
diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs
index 4bf132d82c963..e6790329959e6 100644
--- a/polkadot/runtime/westend/src/lib.rs
+++ b/polkadot/runtime/westend/src/lib.rs
@@ -109,7 +109,7 @@ use xcm::{
 use xcm_builder::PayOverXcm;
 
 use xcm_fee_payment_runtime_api::{
-	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	dry_run::{CallDryRunEffects, Error as XcmDryRunApiError, XcmDryRunEffects},
 	fees::Error as XcmPaymentApiError,
 };
 
@@ -2271,63 +2271,13 @@ sp_api::impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
-		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
-			use xcm_builder::InspectMessageQueues;
-			use xcm_executor::RecordXcm;
-			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
-			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
-					"Applying extrinsic failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::InvalidExtrinsic
-			})?;
-			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
-			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
-			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
-			Ok(ExtrinsicDryRunEffects {
-				local_xcm: local_xcm.map(VersionedXcm::<()>::from),
-				forwarded_xcms,
-				emitted_events: events,
-				execution_result: result,
-			})
+	impl xcm_fee_payment_runtime_api::dry_run::DryRunApi<Block, RuntimeCall, RuntimeEvent, OriginCaller> for Runtime {
+		fn dry_run_call(origin: OriginCaller, call: RuntimeCall) -> Result<CallDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			XcmPallet::dry_run_call::<Runtime, xcm_config::XcmRouter, OriginCaller, RuntimeCall>(origin, call)
 		}
 
 		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
-			use xcm_builder::InspectMessageQueues;
-			let origin_location: Location = origin_location.try_into().map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
-					"Location version conversion failed with error: {:?}",
-					error,
-				);
-				XcmDryRunApiError::VersionedConversionFailed
-			})?;
-			let xcm: Xcm<RuntimeCall> = xcm.try_into().map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
-					"Xcm version conversion failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::VersionedConversionFailed
-			})?;
-			let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256);
-			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
-				origin_location,
-				xcm,
-				&mut hash,
-				Weight::MAX, // Max limit available for execution.
-				Weight::zero(),
-			);
-			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
-			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
-			Ok(XcmDryRunEffects {
-				forwarded_xcms,
-				emitted_events: events,
-				execution_result: result,
-			})
+			XcmPallet::dry_run_xcm::<Runtime, xcm_config::XcmRouter, RuntimeCall, xcm_config::XcmConfig>(origin_location, xcm)
 		}
 	}
 
diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs
index 37fc121ba2174..160d52739681f 100644
--- a/polkadot/xcm/pallet-xcm/src/lib.rs
+++ b/polkadot/xcm/pallet-xcm/src/lib.rs
@@ -29,7 +29,9 @@ pub mod migration;
 
 use codec::{Decode, Encode, EncodeLike, MaxEncodedLen};
 use frame_support::{
-	dispatch::{DispatchErrorWithPostInfo, GetDispatchInfo, WithPostDispatchInfo},
+	dispatch::{
+		DispatchErrorWithPostInfo, GetDispatchInfo, PostDispatchInfo, WithPostDispatchInfo,
+	},
 	pallet_prelude::*,
 	traits::{
 		Contains, ContainsPair, Currency, Defensive, EnsureOrigin, Get, LockableCurrency,
@@ -50,18 +52,22 @@ use sp_runtime::{
 use sp_std::{boxed::Box, marker::PhantomData, prelude::*, result::Result, vec};
 use xcm::{latest::QueryResponseInfo, prelude::*};
 use xcm_builder::{
-	ExecuteController, ExecuteControllerWeightInfo, QueryController, QueryControllerWeightInfo,
-	SendController, SendControllerWeightInfo,
+	ExecuteController, ExecuteControllerWeightInfo, InspectMessageQueues, QueryController,
+	QueryControllerWeightInfo, SendController, SendControllerWeightInfo,
 };
 use xcm_executor::{
 	traits::{
 		AssetTransferError, CheckSuspension, ClaimAssets, ConvertLocation, ConvertOrigin,
 		DropAssets, MatchesFungible, OnResponse, Properties, QueryHandler, QueryResponseStatus,
-		TransactAsset, TransferType, VersionChangeNotifier, WeightBounds, XcmAssetTransfers,
+		RecordXcm, TransactAsset, TransferType, VersionChangeNotifier, WeightBounds,
+		XcmAssetTransfers,
 	},
 	AssetsInHolding,
 };
-use xcm_fee_payment_runtime_api::fees::Error as XcmPaymentApiError;
+use xcm_fee_payment_runtime_api::{
+	dry_run::{CallDryRunEffects, Error as XcmDryRunApiError, XcmDryRunEffects},
+	fees::Error as XcmPaymentApiError,
+};
 
 #[cfg(any(feature = "try-runtime", test))]
 use sp_runtime::TryRuntimeError;
@@ -2432,6 +2438,85 @@ impl<T: Config> Pallet<T> {
 		AccountIdConversion::<T::AccountId>::into_account_truncating(&ID)
 	}
 
+	/// Dry-runs `call` with the given `origin`.
+	///
+	/// Returns not only the call result and events, but also the local XCM, if any,
+	/// and any XCMs forwarded to other locations.
+	/// Meant to be used in the `xcm_fee_payment_runtime_api::dry_run::DryRunApi` runtime API.
+	pub fn dry_run_call<Runtime, Router, OriginCaller, RuntimeCall>(
+		origin: OriginCaller,
+		call: RuntimeCall,
+	) -> Result<CallDryRunEffects<<Runtime as frame_system::Config>::RuntimeEvent>, XcmDryRunApiError>
+	where
+		Runtime: crate::Config,
+		Router: InspectMessageQueues,
+		RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo>,
+		<RuntimeCall as Dispatchable>::RuntimeOrigin: From<OriginCaller>,
+	{
+		crate::Pallet::<Runtime>::set_record_xcm(true);
+		frame_system::Pallet::<Runtime>::reset_events(); // To make sure we only record events from current call.
+		let result = call.dispatch(origin.into());
+		crate::Pallet::<Runtime>::set_record_xcm(false);
+		let local_xcm = crate::Pallet::<Runtime>::recorded_xcm();
+		let forwarded_xcms = Router::get_messages();
+		let events: Vec<<Runtime as frame_system::Config>::RuntimeEvent> =
+			frame_system::Pallet::<Runtime>::read_events_no_consensus()
+				.map(|record| record.event.clone())
+				.collect();
+		Ok(CallDryRunEffects {
+			local_xcm: local_xcm.map(VersionedXcm::<()>::from),
+			forwarded_xcms,
+			emitted_events: events,
+			execution_result: result,
+		})
+	}
+
+	/// Dry-runs `xcm` with the given `origin_location`.
+	///
+	/// Returns execution result, events, and any forwarded XCMs to other locations.
+	/// Meant to be used in the `xcm_fee_payment_runtime_api::dry_run::DryRunApi` runtime API.
+	pub fn dry_run_xcm<Runtime, Router, RuntimeCall, XcmConfig>(
+		origin_location: VersionedLocation,
+		xcm: VersionedXcm<RuntimeCall>,
+	) -> Result<XcmDryRunEffects<<Runtime as frame_system::Config>::RuntimeEvent>, XcmDryRunApiError>
+	where
+		Runtime: frame_system::Config,
+		Router: InspectMessageQueues,
+		XcmConfig: xcm_executor::Config<RuntimeCall = RuntimeCall>,
+	{
+		let origin_location: Location = origin_location.try_into().map_err(|error| {
+			log::error!(
+				target: "xcm::DryRunApi::dry_run_xcm",
+				"Location version conversion failed with error: {:?}",
+				error,
+			);
+			XcmDryRunApiError::VersionedConversionFailed
+		})?;
+		let xcm: Xcm<RuntimeCall> = xcm.try_into().map_err(|error| {
+			log::error!(
+				target: "xcm::DryRunApi::dry_run_xcm",
+				"Xcm version conversion failed with error {:?}",
+				error,
+			);
+			XcmDryRunApiError::VersionedConversionFailed
+		})?;
+		let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256);
+		frame_system::Pallet::<Runtime>::reset_events(); // To make sure we only record events from current call.
+		let result = xcm_executor::XcmExecutor::<XcmConfig>::prepare_and_execute(
+			origin_location,
+			xcm,
+			&mut hash,
+			Weight::MAX, // Max limit available for execution.
+			Weight::zero(),
+		);
+		let forwarded_xcms = Router::get_messages();
+		let events: Vec<<Runtime as frame_system::Config>::RuntimeEvent> =
+			frame_system::Pallet::<Runtime>::read_events_no_consensus()
+				.map(|record| record.event.clone())
+				.collect();
+		Ok(XcmDryRunEffects { forwarded_xcms, emitted_events: events, execution_result: result })
+	}
+
 	pub fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, XcmPaymentApiError> {
 		let message = Xcm::<()>::try_from(message)
 			.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?;
@@ -3126,7 +3211,7 @@ impl<T: Config> CheckSuspension for Pallet<T> {
 	}
 }
 
-impl<T: Config> xcm_executor::traits::RecordXcm for Pallet<T> {
+impl<T: Config> RecordXcm for Pallet<T> {
 	fn should_record() -> bool {
 		ShouldRecordXcm::<T>::get()
 	}
diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs
index 62a422d6efeb0..9828acab40230 100644
--- a/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs
+++ b/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs
@@ -19,16 +19,15 @@
 //! that need to be paid.
 
 use codec::{Decode, Encode};
-use frame_support::pallet_prelude::{DispatchResult, TypeInfo};
-use sp_runtime::traits::Block as BlockT;
+use frame_support::pallet_prelude::{DispatchResultWithPostInfo, TypeInfo};
 use sp_std::vec::Vec;
 use xcm::prelude::*;
 
 /// Effects of dry-running an extrinsic.
 #[derive(Encode, Decode, Debug, TypeInfo)]
-pub struct ExtrinsicDryRunEffects<Event> {
+pub struct CallDryRunEffects<Event> {
 	/// The result of executing the extrinsic.
-	pub execution_result: DispatchResult,
+	pub execution_result: DispatchResultWithPostInfo,
 	/// The list of events fired by the extrinsic.
 	pub emitted_events: Vec<Event>,
 	/// The local XCM that was attempted to be executed, if any.
@@ -55,12 +54,12 @@ sp_api::decl_runtime_apis! {
 	/// If there's local execution, the location will be "Here".
 	/// This vector can be used to calculate both execution and delivery fees.
 	///
-	/// Extrinsics or XCMs might fail when executed, this doesn't mean the result of these calls will be an `Err`.
+	/// Calls or XCMs might fail when executed, this doesn't mean the result of these calls will be an `Err`.
 	/// In those cases, there might still be a valid result, with the execution error inside it.
 	/// The only reasons why these calls might return an error are listed in the [`Error`] enum.
-	pub trait XcmDryRunApi<Call, Event: Decode> {
-		/// Dry run extrinsic.
-		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<Event>, Error>;
+	pub trait DryRunApi<Call: Encode, Event: Decode, OriginCaller: Encode> {
+		/// Dry run call.
+		fn dry_run_call(origin: OriginCaller, call: Call) -> Result<CallDryRunEffects<Event>, Error>;
 
 		/// Dry run XCM program
 		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<Call>) -> Result<XcmDryRunEffects<Event>, Error>;
@@ -76,8 +75,4 @@ pub enum Error {
 	/// Converting a versioned data structure from one version to another failed.
 	#[codec(index = 1)]
 	VersionedConversionFailed,
-
-	/// Extrinsic was invalid.
-	#[codec(index = 2)]
-	InvalidExtrinsic,
 }
diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs
index 25a68090c22f5..33611c8a471c0 100644
--- a/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs
+++ b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs
@@ -16,19 +16,17 @@
 
 //! Tests for using both the XCM fee payment API and the dry-run API.
 
-use frame_support::{
-	dispatch::DispatchInfo,
-	pallet_prelude::{DispatchClass, Pays},
-};
+use frame_system::RawOrigin;
 use sp_api::ProvideRuntimeApi;
 use sp_runtime::testing::H256;
 use xcm::prelude::*;
-use xcm_fee_payment_runtime_api::{dry_run::XcmDryRunApi, fees::XcmPaymentApi};
+use xcm_fee_payment_runtime_api::{dry_run::DryRunApi, fees::XcmPaymentApi};
 
 mod mock;
 use mock::{
-	extra, fake_message_hash, new_test_ext_with_balances, new_test_ext_with_balances_and_assets,
-	DeliveryFees, ExistentialDeposit, HereLocation, RuntimeCall, RuntimeEvent, TestClient, TestXt,
+	fake_message_hash, new_test_ext_with_balances, new_test_ext_with_balances_and_assets,
+	DeliveryFees, ExistentialDeposit, HereLocation, OriginCaller, RuntimeCall, RuntimeEvent,
+	TestClient,
 };
 
 // Scenario: User `1` in the local chain (id 2000) wants to transfer assets to account `[0u8; 32]`
@@ -50,24 +48,22 @@ fn fee_estimation_for_teleport() {
 	new_test_ext_with_balances_and_assets(balances, assets).execute_with(|| {
 		let client = TestClient;
 		let runtime_api = client.runtime_api();
-		let extrinsic = TestXt::new(
-			RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets {
-				dest: Box::new(VersionedLocation::from((Parent, Parachain(1000)))),
-				beneficiary: Box::new(VersionedLocation::from(AccountId32 {
-					id: [0u8; 32],
-					network: None,
-				})),
-				assets: Box::new(VersionedAssets::from(vec![
-					(Here, 100u128).into(),
-					(Parent, 20u128).into(),
-				])),
-				fee_asset_item: 1, // Fees are paid with the RelayToken
-				weight_limit: Unlimited,
-			}),
-			Some((who, extra())),
-		);
+		let call = RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets {
+			dest: Box::new(VersionedLocation::from((Parent, Parachain(1000)))),
+			beneficiary: Box::new(VersionedLocation::from(AccountId32 {
+				id: [0u8; 32],
+				network: None,
+			})),
+			assets: Box::new(VersionedAssets::from(vec![
+				(Here, 100u128).into(),
+				(Parent, 20u128).into(),
+			])),
+			fee_asset_item: 1, // Fees are paid with the RelayToken
+			weight_limit: Unlimited,
+		});
+		let origin = OriginCaller::system(RawOrigin::Signed(who));
 		let dry_run_effects =
-			runtime_api.dry_run_extrinsic(H256::zero(), extrinsic).unwrap().unwrap();
+			runtime_api.dry_run_call(H256::zero(), origin, call).unwrap().unwrap();
 
 		assert_eq!(
 			dry_run_effects.local_xcm,
@@ -130,14 +126,6 @@ fn fee_estimation_for_teleport() {
 					message: send_message.clone(),
 					message_id: fake_message_hash(&send_message),
 				}),
-				RuntimeEvent::System(frame_system::Event::ExtrinsicSuccess {
-					dispatch_info: DispatchInfo {
-						weight: Weight::from_parts(107074070, 0), /* Will break if weights get
-						                                           * updated. */
-						class: DispatchClass::Normal,
-						pays_fee: Pays::Yes,
-					}
-				}),
 			]
 		);
 
@@ -216,21 +204,19 @@ fn dry_run_reserve_asset_transfer() {
 	new_test_ext_with_balances_and_assets(balances, assets).execute_with(|| {
 		let client = TestClient;
 		let runtime_api = client.runtime_api();
-		let extrinsic = TestXt::new(
-			RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets {
-				dest: Box::new(VersionedLocation::from((Parent, Parachain(1000)))),
-				beneficiary: Box::new(VersionedLocation::from(AccountId32 {
-					id: [0u8; 32],
-					network: None,
-				})),
-				assets: Box::new(VersionedAssets::from((Parent, 100u128))),
-				fee_asset_item: 0,
-				weight_limit: Unlimited,
-			}),
-			Some((who, extra())),
-		);
+		let call = RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets {
+			dest: Box::new(VersionedLocation::from((Parent, Parachain(1000)))),
+			beneficiary: Box::new(VersionedLocation::from(AccountId32 {
+				id: [0u8; 32],
+				network: None,
+			})),
+			assets: Box::new(VersionedAssets::from((Parent, 100u128))),
+			fee_asset_item: 0,
+			weight_limit: Unlimited,
+		});
+		let origin = OriginCaller::system(RawOrigin::Signed(who));
 		let dry_run_effects =
-			runtime_api.dry_run_extrinsic(H256::zero(), extrinsic).unwrap().unwrap();
+			runtime_api.dry_run_call(H256::zero(), origin, call).unwrap().unwrap();
 
 		assert_eq!(
 			dry_run_effects.local_xcm,
@@ -281,14 +267,6 @@ fn dry_run_reserve_asset_transfer() {
 					message: send_message.clone(),
 					message_id: fake_message_hash(&send_message),
 				}),
-				RuntimeEvent::System(frame_system::Event::ExtrinsicSuccess {
-					dispatch_info: DispatchInfo {
-						weight: Weight::from_parts(107074066, 0), /* Will break if weights get
-						                                           * updated. */
-						class: DispatchClass::Normal,
-						pays_fee: Pays::Yes,
-					}
-				}),
 			]
 		);
 	});
diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs
index a1794ab99de71..aa6c1422b608c 100644
--- a/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs
+++ b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs
@@ -29,7 +29,7 @@ use frame_support::{
 use frame_system::{EnsureRoot, RawOrigin as SystemRawOrigin};
 use pallet_xcm::TestWeightInfo;
 use sp_runtime::{
-	traits::{Block as BlockT, Get, IdentityLookup, MaybeEquivalence, TryConvert},
+	traits::{Dispatchable, Get, IdentityLookup, MaybeEquivalence, TryConvert},
 	BuildStorage, SaturatedConversion,
 };
 use sp_std::{cell::RefCell, marker::PhantomData};
@@ -45,7 +45,7 @@ use xcm_executor::{
 };
 
 use xcm_fee_payment_runtime_api::{
-	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunApi, XcmDryRunEffects},
+	dry_run::{CallDryRunEffects, DryRunApi, Error as XcmDryRunApiError, XcmDryRunEffects},
 	fees::{Error as XcmPaymentApiError, XcmPaymentApi},
 };
 
@@ -58,30 +58,13 @@ construct_runtime! {
 	}
 }
 
-pub type SignedExtra = (
-	// frame_system::CheckEra<TestRuntime>,
-	// frame_system::CheckNonce<TestRuntime>,
-	frame_system::CheckWeight<TestRuntime>,
-);
+pub type SignedExtra = (frame_system::CheckWeight<TestRuntime>,);
 pub type TestXt = sp_runtime::testing::TestXt<RuntimeCall, SignedExtra>;
 type Block = sp_runtime::testing::Block<TestXt>;
 type Balance = u128;
 type AssetIdForAssetsPallet = u32;
 type AccountId = u64;
 
-pub fn extra() -> SignedExtra {
-	(frame_system::CheckWeight::new(),)
-}
-
-type Executive = frame_executive::Executive<
-	TestRuntime,
-	Block,
-	frame_system::ChainContext<TestRuntime>,
-	TestRuntime,
-	AllPalletsWithSystem,
-	(),
->;
-
 #[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
 impl frame_system::Config for TestRuntime {
 	type Block = Block;
@@ -467,29 +450,21 @@ sp_api::mock_impl_runtime_apis! {
 		}
 	}
 
-	impl XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for RuntimeApi {
-		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+	impl DryRunApi<Block, RuntimeCall, RuntimeEvent, OriginCaller> for RuntimeApi {
+		fn dry_run_call(origin: OriginCaller, call: RuntimeCall) -> Result<CallDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
 			use xcm_executor::RecordXcm;
-			// We want to record the XCM that's executed, so we can return it.
 			pallet_xcm::Pallet::<TestRuntime>::set_record_xcm(true);
-			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
-				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
-					"Applying extrinsic failed with error {:?}",
-					error,
-				);
-				XcmDryRunApiError::InvalidExtrinsic
-			})?;
-			// Nothing gets committed to storage in runtime APIs, so there's no harm in leaving the flag as true.
+			let result = call.dispatch(origin.into());
+			pallet_xcm::Pallet::<TestRuntime>::set_record_xcm(false);
 			let local_xcm = pallet_xcm::Pallet::<TestRuntime>::recorded_xcm();
 			let forwarded_xcms = sent_xcm()
-				.into_iter()
-				.map(|(location, message)| (
-					VersionedLocation::from(location),
-					vec![VersionedXcm::from(message)],
-				)).collect();
-			let events: Vec<RuntimeEvent> = System::events().iter().map(|record| record.event.clone()).collect();
-			Ok(ExtrinsicDryRunEffects {
+							   .into_iter()
+							   .map(|(location, message)| (
+									   VersionedLocation::from(location),
+									   vec![VersionedXcm::from(message)],
+							   )).collect();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(CallDryRunEffects {
 				local_xcm: local_xcm.map(VersionedXcm::<()>::from),
 				forwarded_xcms,
 				emitted_events: events,
@@ -500,7 +475,7 @@ sp_api::mock_impl_runtime_apis! {
 		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
 			let origin_location: Location = origin_location.try_into().map_err(|error| {
 				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					target: "xcm::DryRunApi::dry_run_xcm",
 					"Location version conversion failed with error: {:?}",
 					error,
 				);
@@ -508,7 +483,7 @@ sp_api::mock_impl_runtime_apis! {
 			})?;
 			let xcm: Xcm<RuntimeCall> = xcm.try_into().map_err(|error| {
 				log::error!(
-					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					target: "xcm::DryRunApi::dry_run_xcm",
 					"Xcm version conversion failed with error {:?}",
 					error,
 				);
diff --git a/prdoc/pr_4621.prdoc b/prdoc/pr_4621.prdoc
new file mode 100644
index 0000000000000..ebc06b92b39c3
--- /dev/null
+++ b/prdoc/pr_4621.prdoc
@@ -0,0 +1,43 @@
+# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
+# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
+
+title: Change XcmDryRunApi::dry_run_extrinsic to take a call instead
+
+doc:
+  - audience: Runtime User
+    description: |
+      The XcmDryRunApi now dry-run calls instead of extrinsics.
+      This means it's possible to dry-run an extrinsic before signing it,
+      allowing for seamless dry-running in dapps.
+      Additionally, calls can now be dry-run for different accounts.
+  - audience: Runtime Dev
+    description: |
+      The XcmDryRunApi::dry_run_extrinsic function was replaced by
+      XcmDryRunApi::dry_run_call.
+      This new function takes an origin (OriginCaller, the encodable inner variant)
+      and a call instead of an extrinsic.
+      This was needed to not require the user signing twice, once for the dry-run and
+      a second time to actually submit the extrinsic.
+      Additionally, calls can now be dry-run for different accounts.
+      The implementation for this runtime API is now simpler, being `call.dispatch(origin.into())`
+      instead of using the `Executive`.
+
+crates:
+  - name: xcm-fee-payment-runtime-api
+    bump: major
+  - name: penpal-runtime
+    bump: major
+  - name: xcm-emulator
+    bump: minor
+  - name: polkadot-service
+    bump: major
+  - name: rococo-runtime
+    bump: major
+  - name: westend-runtime
+    bump: major
+  - name: asset-hub-rococo-runtime
+    bump: major
+  - name: asset-hub-westend-runtime
+    bump: major
+  - name: pallet-xcm
+    bump: minor