diff --git a/.snippets/code/develop/interoperability/xcm-observability/execution-with-error.html b/.snippets/code/develop/interoperability/xcm-observability/execution-with-error.html
new file mode 100644
index 000000000..e00aef25b
--- /dev/null
+++ b/.snippets/code/develop/interoperability/xcm-observability/execution-with-error.html
@@ -0,0 +1,19 @@
+
+
+"error": {
+ "type": "Module",
+ "value": {
+ "type": "PolkadotXcm",
+ "value": {
+ "type": "LocalExecutionIncompleteWithError",
+ "value": {
+ "index": 0,
+ "error": {
+ "type": "FailedToTransactAsset"
+ }
+ }
+ }
+ }
+}
+
+
diff --git a/.snippets/code/develop/interoperability/xcm-observability/forward-id-for.ts b/.snippets/code/develop/interoperability/xcm-observability/forward-id-for.ts
new file mode 100644
index 000000000..7784e3e15
--- /dev/null
+++ b/.snippets/code/develop/interoperability/xcm-observability/forward-id-for.ts
@@ -0,0 +1,32 @@
+import {blake2b} from "@noble/hashes/blake2";
+import {fromHex, mergeUint8, toHex} from "@polkadot-api/utils";
+import {Binary} from "polkadot-api";
+
+function forwardIdFor(originalMessageId: string): string {
+ // Decode the hex original_id into bytes
+ const messageIdBytes = fromHex(originalMessageId);
+
+ // Create prefixed input: b"forward_id_for" + original_id
+ const prefix = Binary.fromText("forward_id_for").asBytes();
+ const input = mergeUint8([prefix, messageIdBytes]);
+
+ // Hash it using blake2b with 32-byte output
+ const forwardedIdBytes = blake2b(input, {dkLen: 32});
+ // Convert to hex
+ return toHex(forwardedIdBytes);
+}
+
+// Example: Forwarded ID from an original_id
+const originalMessageId = "0x5c082b4750ee8c34986eb22ce6e345bad2360f3682cda3e99de94b0d9970cb3e";
+
+// Create the forwarded ID
+const forwardedIdHex = forwardIdFor(originalMessageId);
+
+console.log("🔄 Forwarded ID:", forwardedIdHex);
+
+const expectedForwardedId = "0xb3ae32fd2e2f798e8215865a8950d19df8330843608d4ee44e9f86849029724a";
+if (forwardedIdHex === expectedForwardedId) {
+ console.log("✅ Forwarded ID matches expected value.");
+} else {
+ console.error("❌ Forwarded ID does not match expected value.");
+}
\ No newline at end of file
diff --git a/.snippets/code/tutorials/interoperability/xcm-observability-in-action/deposit-reserve-asset-with-set-topic-result.html b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/deposit-reserve-asset-with-set-topic-result.html
new file mode 100644
index 000000000..99199a40e
--- /dev/null
+++ b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/deposit-reserve-asset-with-set-topic-result.html
@@ -0,0 +1,10 @@
+
+ npx tsx limited-reserve-transfer-assets.ts
+ ✅ Local dry run successful.
+ 📦 Finalised on Polkadot Hub in block #9471830: 0x98bd858739b3b5dd558def60cbd85d5e7fb2f4e33b0c00e1895e316541d727d9
+ 📣 Last message sent on Polkadot Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Sent Message ID on Polkadot Hub matched.
+ 📦 Finalised on Hydration in block #8749233: 0xe1413c5126698d7189d6f55a38e62d07ea4915078c2b1f3914d70f670e79e162
+ 📣 Last message processed on Hydration: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Processed Message ID on Hydration matched.
+
diff --git a/.snippets/code/tutorials/interoperability/xcm-observability-in-action/deposit-reserve-asset-with-set-topic.ts b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/deposit-reserve-asset-with-set-topic.ts
new file mode 100644
index 000000000..e16b5d9b6
--- /dev/null
+++ b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/deposit-reserve-asset-with-set-topic.ts
@@ -0,0 +1,236 @@
+import {Binary, createClient, Enum, type BlockInfo, type PolkadotClient} from "polkadot-api";
+import {withPolkadotSdkCompat} from "polkadot-api/polkadot-sdk-compat";
+import {getPolkadotSigner} from "polkadot-api/signer";
+import {getWsProvider} from "polkadot-api/ws-provider/web";
+import {
+ assetHub,
+ hydration,
+ XcmV3MultiassetFungibility,
+ XcmV3WeightLimit,
+ XcmV5AssetFilter,
+ XcmV5Instruction,
+ XcmV5Junction,
+ XcmV5Junctions,
+ XcmV5WildAsset,
+ XcmVersionedXcm,
+} from "@polkadot-api/descriptors";
+import {sr25519CreateDerive} from "@polkadot-labs/hdkd";
+import {
+ DEV_PHRASE,
+ entropyToMiniSecret,
+ mnemonicToEntropy,
+ ss58Address,
+} from "@polkadot-labs/hdkd-helpers";
+
+const XCM_VERSION = 5;
+const MAX_RETRIES = 8; // Number of attempts to wait for block finalisation
+
+const toHuman = (_key: any, value: any) => {
+ if (typeof value === "bigint") {
+ return Number(value);
+ }
+
+ if (value && typeof value === "object" && typeof value.asHex === "function") {
+ return value.asHex();
+ }
+
+ return value;
+};
+
+async function assertProcessedMessageId(
+ client: PolkadotClient,
+ api: any,
+ name: String,
+ blockBefore: BlockInfo,
+ expectedMessageId: String,
+) {
+ let processedMessageId = undefined;
+ for (let i = 0; i < MAX_RETRIES; i++) {
+ const blockAfter = await client.getFinalizedBlock();
+ if (blockAfter.number == blockBefore.number) {
+ const waiting = 1_000 * (2 ** i);
+ console.log(`⏳ Waiting ${waiting / 1_000}s for ${name} block to be finalised (${i + 1}/${MAX_RETRIES})...`);
+ await new Promise((resolve) => setTimeout(resolve, waiting));
+ continue;
+ }
+
+ console.log(`📦 Finalised on ${name} in block #${blockAfter.number}: ${blockAfter.hash}`);
+ const processedEvents = await api.event.MessageQueue.Processed.pull();
+ const processingFailedEvents = await api.event.MessageQueue.ProcessingFailed.pull();
+ if (processedEvents.length > 0) {
+ processedMessageId = processedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message processed on ${name}: ${processedMessageId}`);
+ break;
+ } else if (processingFailedEvents.length > 0) {
+ processedMessageId = processingFailedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message ProcessingFailed on ${name}: ${processedMessageId}`);
+ break;
+ } else {
+ console.log(`📣 No Processed events on ${name} found.`);
+ blockBefore = blockAfter; // Update the block before to the latest one
+ }
+ }
+
+ if (processedMessageId === expectedMessageId) {
+ console.log(`✅ Processed Message ID on ${name} matched.`);
+ } else if (processedMessageId === undefined) {
+ console.error(`❌ Processed Message ID on ${name} is undefined. Try increasing MAX_RETRIES to wait for block finalisation.`);
+ } else {
+ console.error(`❌ Processed Message ID [${processedMessageId}] on ${name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+}
+
+async function main() {
+ const para1Name = "Polkadot Hub";
+ const para1Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8000")),
+ );
+ const para1Api = para1Client.getTypedApi(assetHub);
+
+ const para2Name = "Hydration";
+ const para2Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8001")),
+ );
+ const para2Api = para2Client.getTypedApi(hydration);
+
+ const entropy = mnemonicToEntropy(DEV_PHRASE);
+ const miniSecret = entropyToMiniSecret(entropy);
+ const derive = sr25519CreateDerive(miniSecret);
+ const alice = derive("//Alice");
+ const alicePublicKey = alice.publicKey;
+ const aliceSigner = getPolkadotSigner(alicePublicKey, "Sr25519", alice.sign);
+ const aliceAddress = ss58Address(alicePublicKey);
+
+ const origin = Enum("system", Enum("Signed", aliceAddress));
+ const beneficiary = {
+ parents: 0,
+ interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
+ id: Binary.fromHex("0x9818ff3c27d256631065ecabf0c50e02551e5c5342b8669486c1e566fcbf847f")
+ })),
+ }
+ const expectedMessageId = "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2";
+
+ const message = XcmVersionedXcm.V5([
+ XcmV5Instruction.WithdrawAsset([
+ {
+ id: {
+ parents: 1,
+ interior: XcmV5Junctions.Here(),
+ },
+ fun: XcmV3MultiassetFungibility.Fungible(1_000_000_000n),
+ },
+ ]),
+
+ XcmV5Instruction.ClearOrigin(),
+
+ XcmV5Instruction.BuyExecution({
+ fees: {
+ id: {
+ parents: 1,
+ interior: XcmV5Junctions.Here(),
+ },
+ fun: XcmV3MultiassetFungibility.Fungible(500_000_000n),
+ },
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ }),
+
+ XcmV5Instruction.DepositReserveAsset({
+ assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.All()),
+ dest: {
+ parents: 1,
+ interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(2034)),
+ },
+ xcm: [
+ XcmV5Instruction.BuyExecution({
+ fees: {
+ id: {
+ parents: 1,
+ interior: XcmV5Junctions.Here(),
+ },
+ fun: XcmV3MultiassetFungibility.Fungible(500_000_000n),
+ },
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ }),
+
+ XcmV5Instruction.DepositAsset({
+ assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.All()),
+ beneficiary,
+ }),
+ ],
+ }),
+
+ XcmV5Instruction.SetTopic(Binary.fromHex(expectedMessageId)),
+ ]);
+
+ const weight: any = await para1Api.apis.XcmPaymentApi.query_xcm_weight(message);
+ if (weight.success !== true) {
+ console.error("❌ Failed to query XCM weight:", weight.error);
+ para1Client.destroy();
+ return;
+ }
+
+ const tx: any = para1Api.tx.PolkadotXcm.execute({
+ message,
+ max_weight: weight.value,
+ });
+ const decodedCall: any = tx.decodedCall;
+ console.log("👀 Executing XCM:", JSON.stringify(decodedCall, toHuman, 2));
+
+ try {
+ const dryRunResult: any = await para1Api.apis.DryRunApi.dry_run_call(
+ origin,
+ decodedCall,
+ XCM_VERSION,
+ );
+ console.log("📦 Dry run result:", JSON.stringify(dryRunResult.value, toHuman, 2));
+
+ const executionResult = dryRunResult.value.execution_result;
+ if (!dryRunResult.success || !executionResult.success) {
+ console.error("❌ Local dry run failed!");
+ } else {
+ console.log("✅ Local dry run successful.");
+
+ const emittedEvents: [any] = dryRunResult.value.emitted_events;
+ const polkadotXcmSentEvent = emittedEvents.find(event =>
+ event.type === "PolkadotXcm" && event.value.type === "Sent"
+ );
+ if (polkadotXcmSentEvent === undefined) {
+ console.log(`⚠️ PolkadotXcm.Sent is only available in runtimes built from stable2503-5 or later.`);
+ } else {
+ let para2BlockBefore = await para2Client.getFinalizedBlock();
+ const extrinsic = await tx.signAndSubmit(aliceSigner);
+ const para1Block = extrinsic.block;
+ console.log(`📦 Finalised on ${para1Name} in block #${para1Block.number}: ${para1Block.hash}`);
+
+ if (!extrinsic.ok) {
+ const dispatchError = extrinsic.dispatchError;
+ if (dispatchError.type === "Module") {
+ const modErr: any = dispatchError.value;
+ console.error(`❌ Dispatch error in module: ${modErr.type} → ${modErr.value?.type}`);
+ } else {
+ console.error("❌ Dispatch error:", JSON.stringify(dispatchError, toHuman, 2));
+ }
+ }
+
+ const sentEvents: any = await para1Api.event.PolkadotXcm.Sent.pull();
+ if (sentEvents.length > 0) {
+ const sentMessageId = sentEvents[0].payload.message_id.asHex();
+ console.log(`📣 Last message sent on ${para1Name}: ${sentMessageId}`);
+ if (sentMessageId === expectedMessageId) {
+ console.log(`✅ Sent Message ID on ${para1Name} matched.`);
+ } else {
+ console.error(`❌ Sent Message ID [${sentMessageId}] on ${para1Name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+ await assertProcessedMessageId(para2Client, para2Api, para2Name, para2BlockBefore, expectedMessageId);
+ } else {
+ console.log(`📣 No Sent events on ${para1Name} found.`);
+ }
+ }
+ }
+ } finally {
+ para1Client.destroy();
+ para2Client.destroy();
+ }
+}
+
+main().catch(console.error);
\ No newline at end of file
diff --git a/.snippets/code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm-custom-topic.html b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm-custom-topic.html
new file mode 100644
index 000000000..63dd34b02
--- /dev/null
+++ b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm-custom-topic.html
@@ -0,0 +1,25 @@
+
+
+[
+ {
+ "type": "ReserveAssetDeposited",
+ "value": [...]
+ },
+ {
+ "type": "ClearOrigin"
+ },
+ {
+ "type": "BuyExecution",
+ "value": {...}
+ },
+ {
+ "type": "DepositAsset",
+ "value": {...}
+ },
+ {
+ "type": "SetTopic",
+ "value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2"
+ }
+]
+
+
diff --git a/.snippets/code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm-remote-topic.html b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm-remote-topic.html
new file mode 100644
index 000000000..74afcc8ad
--- /dev/null
+++ b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm-remote-topic.html
@@ -0,0 +1,46 @@
+
+
+[
+ {
+ "type": "ReserveAssetDeposited",
+ "value": [...]
+ },
+ {
+ "type": "ClearOrigin"
+ },
+ {
+ "type": "BuyExecution",
+ "value": {...}
+ },
+ {
+ "type": "ExchangeAsset",
+ "value": {...}
+ },
+ {
+ "type": "InitiateReserveWithdraw",
+ "value": {
+ "assets": {...},
+ "reserve": {...},
+ "xcm": [
+ {
+ "type": "BuyExecution",
+ "value": {...}
+ },
+ {
+ "type": "DepositAsset",
+ "value": {...}
+ },
+ {
+ "type": "SetTopic",
+ "value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2"
+ }
+ ]
+ }
+ },
+ {
+ "type": "SetTopic",
+ "value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2"
+ }
+]
+
+
diff --git a/.snippets/code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm.html b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm.html
new file mode 100644
index 000000000..8155efd30
--- /dev/null
+++ b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm.html
@@ -0,0 +1,25 @@
+
+
+[
+ {
+ "type": "ReserveAssetDeposited",
+ "value": [...]
+ },
+ {
+ "type": "ClearOrigin"
+ },
+ {
+ "type": "BuyExecution",
+ "value": {...}
+ },
+ {
+ "type": "DepositAsset",
+ "value": {...}
+ },
+ {
+ "type": "SetTopic",
+ "value": "0x3b5650b78230aebb8f2573d2c5e8356494ab01e39e716087c177bf871dce70b9"
+ }
+]
+
+
diff --git a/.snippets/code/tutorials/interoperability/xcm-observability-in-action/initiate-reserve-withdraw-with-set-topic-result.html b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/initiate-reserve-withdraw-with-set-topic-result.html
new file mode 100644
index 000000000..00ec47faf
--- /dev/null
+++ b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/initiate-reserve-withdraw-with-set-topic-result.html
@@ -0,0 +1,13 @@
+
+ npx tsx initiate-reserve-withdraw-with-set-topic.ts
+ ✅ Local dry run successful.
+ 📦 Finalised on Polkadot Hub in block #9471831: 0x2620f7e29765fc953263b7835711011616702c9d82ef5306fe3ef4196cb75cab
+ 📣 Last message sent on Polkadot Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Sent Message ID on Polkadot Hub matched.
+ 📦 Finalised on Hydration in block #8749235: 0xafe7f6149b1773a8d3d229040cda414aafd64baaeffa37fb4a5b2a542308b2d6
+ 📣 Last message processed on Hydration: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Processed Message ID on Hydration matched.
+ 📦 Finalised on Polkadot Hub in block #9471832: 0x7c150b69e3562694f0573e4fee73dfb86f3ab71b808679a1777586ff24643e9a
+ 📣 Last message processed on Polkadot Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Processed Message ID on Polkadot Hub matched.
+
diff --git a/.snippets/code/tutorials/interoperability/xcm-observability-in-action/initiate-reserve-withdraw-with-set-topic.ts b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/initiate-reserve-withdraw-with-set-topic.ts
new file mode 100644
index 000000000..06dd02a95
--- /dev/null
+++ b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/initiate-reserve-withdraw-with-set-topic.ts
@@ -0,0 +1,291 @@
+import {Binary, createClient, Enum, type BlockInfo, type PolkadotClient} from "polkadot-api";
+import {withPolkadotSdkCompat} from "polkadot-api/polkadot-sdk-compat";
+import {getPolkadotSigner} from "polkadot-api/signer";
+import {getWsProvider} from "polkadot-api/ws-provider/web";
+import {
+ assetHub,
+ hydration,
+ XcmV2MultiassetWildFungibility,
+ XcmV3MultiassetFungibility,
+ XcmV3WeightLimit,
+ XcmV5AssetFilter,
+ XcmV5Instruction,
+ XcmV5Junction,
+ XcmV5Junctions,
+ XcmV5WildAsset,
+ XcmVersionedXcm,
+} from "@polkadot-api/descriptors";
+import {sr25519CreateDerive} from "@polkadot-labs/hdkd";
+import {
+ DEV_PHRASE,
+ entropyToMiniSecret,
+ mnemonicToEntropy,
+ ss58Address,
+} from "@polkadot-labs/hdkd-helpers";
+
+const XCM_VERSION = 5;
+const MAX_RETRIES = 8; // Number of attempts to wait for block finalisation
+
+const toHuman = (_key: any, value: any) => {
+ if (typeof value === "bigint") {
+ return Number(value);
+ }
+
+ if (value && typeof value === "object" && typeof value.asHex === "function") {
+ return value.asHex();
+ }
+
+ return value;
+};
+
+async function assertProcessedMessageId(
+ client: PolkadotClient,
+ api: any,
+ name: String,
+ blockBefore: BlockInfo,
+ expectedMessageId: String,
+) {
+ let processedMessageId = undefined;
+ for (let i = 0; i < MAX_RETRIES; i++) {
+ const blockAfter = await client.getFinalizedBlock();
+ if (blockAfter.number == blockBefore.number) {
+ const waiting = 1_000 * (2 ** i);
+ console.log(`⏳ Waiting ${waiting / 1_000}s for ${name} block to be finalised (${i + 1}/${MAX_RETRIES})...`);
+ await new Promise((resolve) => setTimeout(resolve, waiting));
+ continue;
+ }
+
+ console.log(`📦 Finalised on ${name} in block #${blockAfter.number}: ${blockAfter.hash}`);
+ const processedEvents = await api.event.MessageQueue.Processed.pull();
+ const processingFailedEvents = await api.event.MessageQueue.ProcessingFailed.pull();
+ if (processedEvents.length > 0) {
+ processedMessageId = processedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message processed on ${name}: ${processedMessageId}`);
+ break;
+ } else if (processingFailedEvents.length > 0) {
+ processedMessageId = processingFailedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message ProcessingFailed on ${name}: ${processedMessageId}`);
+ break;
+ } else {
+ console.log(`📣 No Processed events on ${name} found.`);
+ blockBefore = blockAfter; // Update the block before to the latest one
+ }
+ }
+
+ if (processedMessageId === expectedMessageId) {
+ console.log(`✅ Processed Message ID on ${name} matched.`);
+ } else if (processedMessageId === undefined) {
+ console.error(`❌ Processed Message ID on ${name} is undefined. Try increasing MAX_RETRIES to wait for block finalisation.`);
+ } else {
+ console.error(`❌ Processed Message ID [${processedMessageId}] on ${name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+}
+
+async function main() {
+ const para1Name = "Polkadot Hub";
+ const para1Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8000")),
+ );
+ const para1Api = para1Client.getTypedApi(assetHub);
+
+ const para2Name = "Hydration";
+ const para2Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8001")),
+ );
+ const para2Api = para2Client.getTypedApi(hydration);
+
+ const entropy = mnemonicToEntropy(DEV_PHRASE);
+ const miniSecret = entropyToMiniSecret(entropy);
+ const derive = sr25519CreateDerive(miniSecret);
+ const alice = derive("//Alice");
+ const alicePublicKey = alice.publicKey;
+ const aliceSigner = getPolkadotSigner(alicePublicKey, "Sr25519", alice.sign);
+ const aliceAddress = ss58Address(alicePublicKey);
+
+ const origin = Enum("system", Enum("Signed", aliceAddress));
+ const beneficiary = {
+ parents: 0,
+ interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
+ id: Binary.fromHex("0x9818ff3c27d256631065ecabf0c50e02551e5c5342b8669486c1e566fcbf847f")
+ })),
+ }
+ const tokenId = XcmV5Junction.GeneralIndex(1337n); // Change to another token if FailedToTransactAsset("Funds are unavailable")
+ const assetId = {
+ parents: 0,
+ interior: XcmV5Junctions.X2([
+ XcmV5Junction.PalletInstance(50),
+ tokenId,
+ ]),
+ };
+ const giveId = {
+ parents: 1,
+ interior: XcmV5Junctions.X3([
+ XcmV5Junction.Parachain(1000),
+ XcmV5Junction.PalletInstance(50),
+ tokenId,
+ ]),
+ };
+ const giveFun = XcmV3MultiassetFungibility.Fungible(1_000_000n);
+ const dest = {
+ parents: 1,
+ interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(2034)),
+ };
+ const wantId = {
+ parents: 1,
+ interior: XcmV5Junctions.Here(),
+ };
+ const wantFun = XcmV3MultiassetFungibility.Fungible(2_000_000_000n); // Adjust the exchange rate if xcm_error is NoDeal
+ const expectedMessageId = "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2";
+
+ const message = XcmVersionedXcm.V5([
+ XcmV5Instruction.WithdrawAsset([{
+ id: assetId,
+ fun: giveFun,
+ }]),
+
+ XcmV5Instruction.SetFeesMode({jit_withdraw: true}),
+
+ XcmV5Instruction.DepositReserveAsset({
+ assets: XcmV5AssetFilter.Wild(
+ XcmV5WildAsset.AllOf({
+ id: assetId,
+ fun: XcmV2MultiassetWildFungibility.Fungible(),
+ })),
+ dest,
+ xcm: [
+ XcmV5Instruction.BuyExecution({
+ fees: {
+ id: giveId,
+ fun: giveFun,
+ },
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ }),
+
+ XcmV5Instruction.ExchangeAsset({
+ give: XcmV5AssetFilter.Wild(
+ XcmV5WildAsset.AllOf({
+ id: giveId,
+ fun: XcmV2MultiassetWildFungibility.Fungible(),
+ }),
+ ),
+ want: [{
+ id: wantId,
+ fun: wantFun,
+ }],
+ maximal: false,
+ }),
+
+ XcmV5Instruction.InitiateReserveWithdraw({
+ assets: XcmV5AssetFilter.Wild(
+ XcmV5WildAsset.AllOf({
+ id: wantId,
+ fun: XcmV2MultiassetWildFungibility.Fungible(),
+ }),
+ ),
+ reserve: {
+ parents: 1,
+ interior: XcmV5Junctions.X1(
+ XcmV5Junction.Parachain(1000),
+ ),
+ },
+ xcm: [
+ XcmV5Instruction.BuyExecution({
+ fees: {
+ id: wantId,
+ fun: wantFun,
+ },
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ }),
+
+ XcmV5Instruction.DepositAsset({
+ assets: XcmV5AssetFilter.Wild(
+ XcmV5WildAsset.AllOf({
+ id: wantId,
+ fun: XcmV2MultiassetWildFungibility.Fungible(),
+ }),
+ ),
+ beneficiary,
+ }),
+
+ XcmV5Instruction.SetTopic(Binary.fromHex(expectedMessageId)), // Ensure the same topic is also set on remote XCM calls
+ ],
+ }),
+ ],
+ }),
+
+ XcmV5Instruction.SetTopic(Binary.fromHex(expectedMessageId)),
+ ]);
+
+ const weight: any = await para1Api.apis.XcmPaymentApi.query_xcm_weight(message);
+ if (weight.success !== true) {
+ console.error("❌ Failed to query XCM weight:", weight.error);
+ para1Client.destroy();
+ return;
+ }
+
+ const tx: any = para1Api.tx.PolkadotXcm.execute({
+ message,
+ max_weight: weight.value,
+ });
+ const decodedCall: any = tx.decodedCall;
+ console.log("👀 Executing XCM:", JSON.stringify(decodedCall, toHuman, 2));
+
+ try {
+ const dryRunResult: any = await para1Api.apis.DryRunApi.dry_run_call(
+ origin,
+ decodedCall,
+ XCM_VERSION,
+ );
+ console.log("📦 Dry run result:", JSON.stringify(dryRunResult.value, toHuman, 2));
+
+ const executionResult = dryRunResult.value.execution_result;
+ if (!dryRunResult.success || !executionResult.success) {
+ console.error("❌ Local dry run failed!");
+ } else {
+ console.log("✅ Local dry run successful.");
+
+ const emittedEvents: [any] = dryRunResult.value.emitted_events;
+ const polkadotXcmSentEvent = emittedEvents.find(event =>
+ event.type === "PolkadotXcm" && event.value.type === "Sent"
+ );
+ if (polkadotXcmSentEvent === undefined) {
+ console.log(`⚠️ PolkadotXcm.Sent is only available in runtimes built from stable2503-5 or later.`);
+ } else {
+ let para2BlockBefore = await para2Client.getFinalizedBlock();
+ const extrinsic = await tx.signAndSubmit(aliceSigner);
+ const para1Block = extrinsic.block;
+ console.log(`📦 Finalised on ${para1Name} in block #${para1Block.number}: ${para1Block.hash}`);
+
+ if (!extrinsic.ok) {
+ const dispatchError = extrinsic.dispatchError;
+ if (dispatchError.type === "Module") {
+ const modErr: any = dispatchError.value;
+ console.error(`❌ Dispatch error in module: ${modErr.type} → ${modErr.value?.type}`);
+ } else {
+ console.error("❌ Dispatch error:", JSON.stringify(dispatchError, toHuman, 2));
+ }
+ }
+
+ const sentEvents: any = await para1Api.event.PolkadotXcm.Sent.pull();
+ if (sentEvents.length > 0) {
+ const sentMessageId = sentEvents[0].payload.message_id.asHex();
+ console.log(`📣 Last message sent on ${para1Name}: ${sentMessageId}`);
+ if (sentMessageId === expectedMessageId) {
+ console.log(`✅ Sent Message ID on ${para1Name} matched.`);
+ } else {
+ console.error(`❌ Sent Message ID [${sentMessageId}] on ${para1Name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+ await assertProcessedMessageId(para2Client, para2Api, para2Name, para2BlockBefore, expectedMessageId);
+ await assertProcessedMessageId(para1Client, para1Api, para1Name, para1Block, expectedMessageId);
+ } else {
+ console.log(`📣 No Sent events on ${para1Name} found.`);
+ }
+ }
+ }
+ } finally {
+ para1Client.destroy();
+ para2Client.destroy();
+ }
+}
+
+main().catch(console.error);
\ No newline at end of file
diff --git a/.snippets/code/tutorials/interoperability/xcm-observability-in-action/limited-reserve-transfer-assets-result.html b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/limited-reserve-transfer-assets-result.html
new file mode 100644
index 000000000..ccd27729f
--- /dev/null
+++ b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/limited-reserve-transfer-assets-result.html
@@ -0,0 +1,9 @@
+
+ npx tsx limited-reserve-transfer-assets.ts
+ ✅ Local dry run successful.
+ 📦 Finalised on Polkadot Hub in block #9477291: 0xf54cecc017762c714bbdf3e82d72ed90886257ca17d32ec6dc8ea20e28110af8
+ 📣 Last message sent on Polkadot Hub: 0x20432393771dc049cea4900565a936d169b8ebdd64efa351890766df918615a4
+ 📦 Finalised on Hydration in block #8761211: 0xa4c493ba9328f38174aa7a9ade0779654839e9d3c83b2bafc60d4e5b7de6a00f
+ 📣 Last message processed on Hydration: 0x20432393771dc049cea4900565a936d169b8ebdd64efa351890766df918615a4
+ ✅ Processed Message ID on Hydration matched.
+
diff --git a/.snippets/code/tutorials/interoperability/xcm-observability-in-action/limited-reserve-transfer-assets.ts b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/limited-reserve-transfer-assets.ts
new file mode 100644
index 000000000..c3952a977
--- /dev/null
+++ b/.snippets/code/tutorials/interoperability/xcm-observability-in-action/limited-reserve-transfer-assets.ts
@@ -0,0 +1,186 @@
+import {Binary, createClient, Enum, type BlockInfo, type PolkadotClient} from "polkadot-api";
+import {withPolkadotSdkCompat} from "polkadot-api/polkadot-sdk-compat";
+import {getPolkadotSigner} from "polkadot-api/signer";
+import {getWsProvider} from "polkadot-api/ws-provider/web";
+import {
+ assetHub,
+ hydration,
+ XcmV3MultiassetFungibility,
+ XcmV3WeightLimit,
+ XcmV5Junction,
+ XcmV5Junctions,
+ XcmVersionedAssets,
+ XcmVersionedLocation,
+} from "@polkadot-api/descriptors";
+import {sr25519CreateDerive} from "@polkadot-labs/hdkd";
+import {
+ DEV_PHRASE,
+ entropyToMiniSecret,
+ mnemonicToEntropy,
+ ss58Address,
+} from "@polkadot-labs/hdkd-helpers";
+
+const XCM_VERSION = 5;
+const MAX_RETRIES = 8; // Number of attempts to wait for block finalisation
+
+const toHuman = (_key: any, value: any) => {
+ if (typeof value === "bigint") {
+ return Number(value);
+ }
+
+ if (value && typeof value === "object" && typeof value.asHex === "function") {
+ return value.asHex();
+ }
+
+ return value;
+};
+
+async function assertProcessedMessageId(
+ client: PolkadotClient,
+ api: any,
+ name: String,
+ blockBefore: BlockInfo,
+ expectedMessageId: String,
+) {
+ let processedMessageId = undefined;
+ for (let i = 0; i < MAX_RETRIES; i++) {
+ const blockAfter = await client.getFinalizedBlock();
+ if (blockAfter.number == blockBefore.number) {
+ const waiting = 1_000 * (2 ** i);
+ console.log(`⏳ Waiting ${waiting / 1_000}s for ${name} block to be finalised (${i + 1}/${MAX_RETRIES})...`);
+ await new Promise((resolve) => setTimeout(resolve, waiting));
+ continue;
+ }
+
+ console.log(`📦 Finalised on ${name} in block #${blockAfter.number}: ${blockAfter.hash}`);
+ const processedEvents = await api.event.MessageQueue.Processed.pull();
+ const processingFailedEvents = await api.event.MessageQueue.ProcessingFailed.pull();
+ if (processedEvents.length > 0) {
+ processedMessageId = processedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message processed on ${name}: ${processedMessageId}`);
+ break;
+ } else if (processingFailedEvents.length > 0) {
+ processedMessageId = processingFailedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message ProcessingFailed on ${name}: ${processedMessageId}`);
+ break;
+ } else {
+ console.log(`📣 No Processed events on ${name} found.`);
+ blockBefore = blockAfter; // Update the block before to the latest one
+ }
+ }
+
+ if (processedMessageId === expectedMessageId) {
+ console.log(`✅ Processed Message ID on ${name} matched.`);
+ } else if (processedMessageId === undefined) {
+ console.error(`❌ Processed Message ID on ${name} is undefined. Try increasing MAX_RETRIES to wait for block finalisation.`);
+ } else {
+ console.error(`❌ Processed Message ID [${processedMessageId}] on ${name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+}
+
+async function main() {
+ const para1Name = "Polkadot Hub";
+ const para1Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8000")),
+ );
+ const para1Api = para1Client.getTypedApi(assetHub);
+
+ const para2Name = "Hydration";
+ const para2Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8001")),
+ );
+ const para2Api = para2Client.getTypedApi(hydration);
+
+ const entropy = mnemonicToEntropy(DEV_PHRASE);
+ const miniSecret = entropyToMiniSecret(entropy);
+ const derive = sr25519CreateDerive(miniSecret);
+ const alice = derive("//Alice");
+ const alicePublicKey = alice.publicKey;
+ const aliceSigner = getPolkadotSigner(alicePublicKey, "Sr25519", alice.sign);
+ const aliceAddress = ss58Address(alicePublicKey);
+
+ const origin = Enum("system", Enum("Signed", aliceAddress));
+ const beneficiary = {
+ parents: 0,
+ interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
+ id: Binary.fromHex("0x9818ff3c27d256631065ecabf0c50e02551e5c5342b8669486c1e566fcbf847f")
+ })),
+ }
+
+ const tx: any = para1Api.tx.PolkadotXcm.limited_reserve_transfer_assets({
+ dest: XcmVersionedLocation.V5({
+ parents: 1,
+ interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(2034)),
+ }),
+ beneficiary: XcmVersionedLocation.V5(beneficiary),
+ assets: XcmVersionedAssets.V5([
+ {
+ id: {
+ parents: 0,
+ interior: XcmV5Junctions.X2([
+ XcmV5Junction.PalletInstance(50),
+ XcmV5Junction.GeneralIndex(1984n),
+ ]),
+ },
+ fun: XcmV3MultiassetFungibility.Fungible(500_000_000n),
+ },
+ ]),
+ fee_asset_item: 0,
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ });
+ const decodedCall: any = tx.decodedCall;
+ console.log("👀 Executing XCM:", JSON.stringify(decodedCall, toHuman, 2));
+
+ try {
+ const dryRunResult: any = await para1Api.apis.DryRunApi.dry_run_call(
+ origin,
+ decodedCall,
+ XCM_VERSION,
+ );
+ console.log("📦 Dry run result:", JSON.stringify(dryRunResult.value, toHuman, 2));
+
+ const executionResult = dryRunResult.value.execution_result;
+ if (!dryRunResult.success || !executionResult.success) {
+ console.error("❌ Local dry run failed!");
+ } else {
+ console.log("✅ Local dry run successful.");
+
+ const emittedEvents: [any] = dryRunResult.value.emitted_events;
+ const polkadotXcmSentEvent = emittedEvents.find(event =>
+ event.type === "PolkadotXcm" && event.value.type === "Sent"
+ );
+ if (polkadotXcmSentEvent === undefined) {
+ console.log(`⚠️ PolkadotXcm.Sent is only available in runtimes built from stable2503-5 or later.`);
+ } else {
+ let para2BlockBefore = await para2Client.getFinalizedBlock();
+ const extrinsic = await tx.signAndSubmit(aliceSigner);
+ const para1Block = extrinsic.block;
+ console.log(`📦 Finalised on ${para1Name} in block #${para1Block.number}: ${para1Block.hash}`);
+
+ if (!extrinsic.ok) {
+ const dispatchError = extrinsic.dispatchError;
+ if (dispatchError.type === "Module") {
+ const modErr: any = dispatchError.value;
+ console.error(`❌ Dispatch error in module: ${modErr.type} → ${modErr.value?.type}`);
+ } else {
+ console.error("❌ Dispatch error:", JSON.stringify(dispatchError, toHuman, 2));
+ }
+ }
+
+ const sentEvents: any = await para1Api.event.PolkadotXcm.Sent.pull();
+ if (sentEvents.length > 0) {
+ const sentMessageId = sentEvents[0].payload.message_id.asHex();
+ console.log(`📣 Last message sent on ${para1Name}: ${sentMessageId}`);
+ await assertProcessedMessageId(para2Client, para2Api, para2Name, para2BlockBefore, sentMessageId);
+ } else {
+ console.log(`📣 No Sent events on ${para1Name} found.`);
+ }
+ }
+ }
+ } finally {
+ para1Client.destroy();
+ para2Client.destroy();
+ }
+}
+
+main().catch(console.error);
\ No newline at end of file
diff --git a/develop/interoperability/.nav.yml b/develop/interoperability/.nav.yml
index 0e53c1d92..73be133c0 100644
--- a/develop/interoperability/.nav.yml
+++ b/develop/interoperability/.nav.yml
@@ -6,6 +6,7 @@ nav:
- 'XCM Runtime Configuration': xcm-config.md
- 'Send Messages': send-messages.md
- 'XCM Runtime APIs': xcm-runtime-apis.md
+ - 'XCM Observability': xcm-observability.md
- 'Test and Debug': test-and-debug.md
- xcm-guides
- versions
diff --git a/develop/interoperability/xcm-observability.md b/develop/interoperability/xcm-observability.md
new file mode 100644
index 000000000..28104830c
--- /dev/null
+++ b/develop/interoperability/xcm-observability.md
@@ -0,0 +1,140 @@
+---
+title: XCM Observability
+description: A conceptual overview of XCM observability in Polkadot covers message correlation, tracing, and debugging features in modern runtimes.
+---
+
+# XCM Observability
+
+## Introduction
+
+Cross-Consensus Messaging (XCM) enables interoperability across the Polkadot ecosystem; however, tracing and debugging these flows across multiple chains is a challenging task. This overview introduces the observability features in modern Polkadot runtimes and the Polkadot SDK that make XCMs easier to trace and debug, including:
+
+- The [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank} instruction and `message_id` to track XCMs across chains.
+- The use of [`PolkadotXcm.Sent`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank} and [`MessageQueue.Processed`](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank} events to correlate messages.
+- Derived message ID workarounds for older runtimes.
+- The use of indexers and Chopsticks replay to diagnose failed or incomplete XCMs.
+
+!!! tip
+ For a hands-on walkthrough, see the companion tutorial: [XCM Observability in Action](/tutorials/interoperability/xcm-observability-in-action){target=\_blank}.
+
+## Topic Register
+
+[`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank} is an XCM instruction that sets a 32-byte topic register inside the message. The topic serves as a logical identifier, or `message_id`, for XCM, allowing you to group and trace related messages across chains.
+
+When executing XCMs (via [`limited_reserve_transfer_assets`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/struct.Pallet.html#method.limited_reserve_transfer_assets){target=\_blank}, other extrinsics, or raw calls from the [`PolkadotXcm` pallet](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/struct.Pallet.html){target=\_blank}) on system chains, the runtime automatically appends a [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank} instruction if one is missing.
+
+!!! note
+ When using [`WithUniqueTopic`](https://paritytech.github.io/polkadot-sdk/master/staging_xcm_builder/struct.WithUniqueTopic.html){target=\_blank}, the topic ID is guaranteed to be unique if `WithUniqueTopic` automatically appends the `SetTopic` instruction. However, if the message already includes a `SetTopic` instruction, the uniqueness of the ID depends on the message creator and is not guaranteed.
+
+## Observability Features
+
+Runtimes built from `stable2503-5` or later provide key observability features for tracing and correlating XCMs across chains:
+
+- [**`PolkadotXcm.Sent`**](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank}: Emitted on the origin chain when an XCM is sent.
+- [**`MessageQueue.Processed`**](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank}: Emitted on the destination chain when the XCM is processed.
+- The `message_id` in the `Sent` event matches the `id` in the `Processed` event, enabling reliable cross-chain correlation. This `message_id` is derived from the `SetTopic` instruction.
+
+| Chain Role | Event | Field | Purpose |
+|-------------------|--------------------------|--------------|------------------------------------|
+| Origin chain | `PolkadotXcm.Sent` | `message_id` | Identifies the sent XCM |
+| Destination chain | `MessageQueue.Processed` | `id` | Confirms processing of the message |
+
+Matching these IDs allows you to correlate an origin message with its destination processing.
+
+!!! question "Why not use `XcmpQueue.XcmpMessageSent`?"
+ The event [`XcmpQueue.XcmpMessageSent`](https://paritytech.github.io/polkadot-sdk/master/cumulus_pallet_xcmp_queue/pallet/enum.Event.html#variant.XcmpMessageSent){target=\_blank} contains a `message_hash` unrelated to `SetTopic`. It is not reliable for cross-chain tracing and should be avoided for observability purposes.
+
+## Automatic vs Manual `SetTopic`
+
+- The runtime automatically appends a `SetTopic` instruction if one is missing at the end of an XCM.
+- When using high-level extrinsics such as `limited_reserve_transfer_assets`, you do not need to set a topic manually; the runtime handles it for you.
+- When manually crafting an XCM via [`execute`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/struct.Pallet.html#method.execute){target=\_blank} or [`send`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/struct.Pallet.html#method.send){target=\_blank}), you can include a `SetTopic` as the final instruction; otherwise, `WithUniqueTopic` will append its own unique ID, overriding earlier `SetTopic` instructions.
+- In multi-hop XCM flows, manually setting the topic ensures consistent tracing across all hops. Any remote XCM calls embedded inside the XCM must also include a `SetTopic` at the end to preserve the same `message_id` throughout the cross-chain flow.
+
+## Message Lifecycle
+
+The following is a high-level overview of an XCM lifecycle with observability events:
+
+```mermaid
+flowchart TD
+ subgraph Origin["Origin Chain"]
+ direction LR
+ A["User submits extrinsic"] --> B["Appends SetTopic if missing"]
+ B --> C["XCM forwarded to destination"]
+ C --> D["Sends XCM"]
+ D --> E["PolkadotXcm.Sent emitted with message_id"]
+ end
+
+ subgraph Destination["Destination Chain"]
+ direction LR
+ F["Process message payload"] --> G["MessageQueue.Processed emitted with matching id"]
+ G --> H["Success/failure logged; further hops if any"]
+ end
+
+ Origin --> Destination
+```
+
+- The `SetTopic` ensures a consistent `message_id` is passed and visible in these events.
+- For multi-hop flows, the same `message_id` travels through all chains.
+
+## Workaround for Older Runtimes
+
+Runtimes prior to `stable2503-5` emit a derived `forwarded_id` instead of the original topic in downstream `Processed` events. The forwarded ID is calculated as:
+
+```rust
+fn forward_id_for(original_id: &XcmHash) -> XcmHash {
+ (b"forward_id_for", original_id).using_encoded(sp_io::hashing::blake2_256)
+}
+```
+
+### Example
+
+| Original `message_id` | Forwarded `message_id` |
+|-----------------------|---------------------------------------------------------------|
+| `0x5c082b47...` | `0xb3ae32fd...` == blake2_256("forward_id_for" + original_id) |
+
+Tools and indexers tracing messages across mixed runtime versions should check both the original and forwarded IDs.
+
+```ts
+--8<-- 'code/develop/interoperability/xcm-observability/forward-id-for.ts'
+```
+
+> Note: `@noble/hashes` and `polkadot-api` are required dependencies for this code to work.
+
+## Failure Event Handling
+
+When an XCM fails, the transaction rolls back and no explicit failure event is emitted on-chain. The following are some ways to check for XCM failure:
+
+1. **View nested dispatch errors via indexers**: Most indexers display nested errors indicating why an XCM failed, e.g.:
+
+ --8<-- 'code/develop/interoperability/xcm-observability/execution-with-error.html'
+
+ Common errors include missing assets, exceeded execution limits, or invalid asset locations. This nested error reporting, introduced in runtimes from `stable2506` onward, usually suffices to diagnose typical issues.
+
+2. **Replay using Chopsticks for full logs**: For detailed troubleshooting, try the following steps:
+
+ - Replay the failing extrinsic with logging enabled.
+ - See exactly which instruction failed and why.
+ - View complete error chains like `FailedToTransactAsset` or `AssetNotFound`.
+
+ This approach is invaluable for multi-hop flows and complex custom XCMs.
+
+## Recommended Debugging Workflow
+
+Follow these steps when debugging XCM failures:
+
+1. Start with indexer or API error output.
+2. If unclear, use Chopsticks to replay the exact transaction.
+3. Inspect logs for the failing XCM instruction and reason.
+4. Adjust weight limits, asset locations, or message construction as needed.
+
+!!! tip
+ See [Replay and Dry Run XCMs Using Chopsticks](/tutorials/interoperability/replay-and-dry-run-xcms/){target=\_blank} for replay instructions.
+
+## Best Practices
+
+- When manually setting `SetTopic`, always place it as the final instruction in your XCM to ensure it is respected by the runtime.
+- Prefer automatic topic insertion via high-level extrinsics for simplicity.
+- If your use case involves multi-hop or custom XCMs, manually set `SetTopic` (including remote XCM calls) to guarantee consistent tracing.
+- Ensure your `message_id` values are unique if you require deduplication or strict correlation.
+- When supporting legacy runtimes, be aware of the `forward_id_for` pattern.
diff --git a/llms-full.txt b/llms-full.txt
index 628f914f5..49bca7a77 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -27,6 +27,7 @@ Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/re
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-guides/from-apps/transact.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-guides/from-apps/transfers.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-guides/index.md
+Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-observability.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-runtime-apis.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/networks.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/parachains/customize-parachain/add-existing-pallets.md
@@ -183,6 +184,7 @@ Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/re
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-channels/para-to-para.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-channels/para-to-system.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-fee-estimation.md
+Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-observability-in-action.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-transfers/from-relaychain-to-parachain.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-transfers/index.md
Doc-Page: https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/onchain-governance/fast-track-gov-proposal.md
@@ -3157,6 +3159,199 @@ Whether you're building applications that need to interact with multiple chains,
--- END CONTENT ---
+Doc-Content: https://docs.polkadot.com/develop/interoperability/xcm-observability/
+--- BEGIN CONTENT ---
+---
+title: XCM Observability
+description: A conceptual overview of XCM observability in Polkadot covers message correlation, tracing, and debugging features in modern runtimes.
+---
+
+# XCM Observability
+
+## Introduction
+
+Cross-Consensus Messaging (XCM) enables interoperability across the Polkadot ecosystem; however, tracing and debugging these flows across multiple chains is a challenging task. This overview introduces the observability features in modern Polkadot runtimes and the Polkadot SDK that make XCMs easier to trace and debug, including:
+
+- The [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank} instruction and `message_id` to track XCMs across chains.
+- The use of [`PolkadotXcm.Sent`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank} and [`MessageQueue.Processed`](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank} events to correlate messages.
+- Derived message ID workarounds for older runtimes.
+- The use of indexers and Chopsticks replay to diagnose failed or incomplete XCMs.
+
+!!! tip
+ For a hands-on walkthrough, see the companion tutorial: [XCM Observability in Action](/tutorials/interoperability/xcm-observability-in-action){target=\_blank}.
+
+## Topic Register
+
+[`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank} is an XCM instruction that sets a 32-byte topic register inside the message. The topic serves as a logical identifier, or `message_id`, for XCM, allowing you to group and trace related messages across chains.
+
+When executing XCMs (via [`limited_reserve_transfer_assets`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/struct.Pallet.html#method.limited_reserve_transfer_assets){target=\_blank}, other extrinsics, or raw calls from the [`PolkadotXcm` pallet](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/struct.Pallet.html){target=\_blank}) on system chains, the runtime automatically appends a [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank} instruction if one is missing.
+
+!!! note
+ When using [`WithUniqueTopic`](https://paritytech.github.io/polkadot-sdk/master/staging_xcm_builder/struct.WithUniqueTopic.html){target=\_blank}, the topic ID is guaranteed to be unique if `WithUniqueTopic` automatically appends the `SetTopic` instruction. However, if the message already includes a `SetTopic` instruction, the uniqueness of the ID depends on the message creator and is not guaranteed.
+
+## Observability Features
+
+Runtimes built from `stable2503-5` or later provide key observability features for tracing and correlating XCMs across chains:
+
+- [**`PolkadotXcm.Sent`**](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank}: Emitted on the origin chain when an XCM is sent.
+- [**`MessageQueue.Processed`**](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank}: Emitted on the destination chain when the XCM is processed.
+- The `message_id` in the `Sent` event matches the `id` in the `Processed` event, enabling reliable cross-chain correlation. This `message_id` is derived from the `SetTopic` instruction.
+
+| Chain Role | Event | Field | Purpose |
+|-------------------|--------------------------|--------------|------------------------------------|
+| Origin chain | `PolkadotXcm.Sent` | `message_id` | Identifies the sent XCM |
+| Destination chain | `MessageQueue.Processed` | `id` | Confirms processing of the message |
+
+Matching these IDs allows you to correlate an origin message with its destination processing.
+
+!!! question "Why not use `XcmpQueue.XcmpMessageSent`?"
+ The event [`XcmpQueue.XcmpMessageSent`](https://paritytech.github.io/polkadot-sdk/master/cumulus_pallet_xcmp_queue/pallet/enum.Event.html#variant.XcmpMessageSent){target=\_blank} contains a `message_hash` unrelated to `SetTopic`. It is not reliable for cross-chain tracing and should be avoided for observability purposes.
+
+## Automatic vs Manual `SetTopic`
+
+- The runtime automatically appends a `SetTopic` instruction if one is missing at the end of an XCM.
+- When using high-level extrinsics such as `limited_reserve_transfer_assets`, you do not need to set a topic manually; the runtime handles it for you.
+- When manually crafting an XCM via [`execute`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/struct.Pallet.html#method.execute){target=\_blank} or [`send`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/struct.Pallet.html#method.send){target=\_blank}), you can include a `SetTopic` as the final instruction; otherwise, `WithUniqueTopic` will append its own unique ID, overriding earlier `SetTopic` instructions.
+- In multi-hop XCM flows, manually setting the topic ensures consistent tracing across all hops. Any remote XCM calls embedded inside the XCM must also include a `SetTopic` at the end to preserve the same `message_id` throughout the cross-chain flow.
+
+## Message Lifecycle
+
+The following is a high-level overview of an XCM lifecycle with observability events:
+
+```mermaid
+flowchart TD
+ subgraph Origin["Origin Chain"]
+ direction LR
+ A["User submits extrinsic"] --> B["Appends SetTopic if missing"]
+ B --> C["XCM forwarded to destination"]
+ C --> D["Sends XCM"]
+ D --> E["PolkadotXcm.Sent emitted with message_id"]
+ end
+
+ subgraph Destination["Destination Chain"]
+ direction LR
+ F["Process message payload"] --> G["MessageQueue.Processed emitted with matching id"]
+ G --> H["Success/failure logged; further hops if any"]
+ end
+
+ Origin --> Destination
+```
+
+- The `SetTopic` ensures a consistent `message_id` is passed and visible in these events.
+- For multi-hop flows, the same `message_id` travels through all chains.
+
+## Workaround for Older Runtimes
+
+Runtimes prior to `stable2503-5` emit a derived `forwarded_id` instead of the original topic in downstream `Processed` events. The forwarded ID is calculated as:
+
+```rust
+fn forward_id_for(original_id: &XcmHash) -> XcmHash {
+ (b"forward_id_for", original_id).using_encoded(sp_io::hashing::blake2_256)
+}
+```
+
+### Example
+
+| Original `message_id` | Forwarded `message_id` |
+|-----------------------|---------------------------------------------------------------|
+| `0x5c082b47...` | `0xb3ae32fd...` == blake2_256("forward_id_for" + original_id) |
+
+Tools and indexers tracing messages across mixed runtime versions should check both the original and forwarded IDs.
+
+```ts
+import {blake2b} from "@noble/hashes/blake2";
+import {fromHex, mergeUint8, toHex} from "@polkadot-api/utils";
+import {Binary} from "polkadot-api";
+
+function forwardIdFor(originalMessageId: string): string {
+ // Decode the hex original_id into bytes
+ const messageIdBytes = fromHex(originalMessageId);
+
+ // Create prefixed input: b"forward_id_for" + original_id
+ const prefix = Binary.fromText("forward_id_for").asBytes();
+ const input = mergeUint8([prefix, messageIdBytes]);
+
+ // Hash it using blake2b with 32-byte output
+ const forwardedIdBytes = blake2b(input, {dkLen: 32});
+ // Convert to hex
+ return toHex(forwardedIdBytes);
+}
+
+// Example: Forwarded ID from an original_id
+const originalMessageId = "0x5c082b4750ee8c34986eb22ce6e345bad2360f3682cda3e99de94b0d9970cb3e";
+
+// Create the forwarded ID
+const forwardedIdHex = forwardIdFor(originalMessageId);
+
+console.log("🔄 Forwarded ID:", forwardedIdHex);
+
+const expectedForwardedId = "0xb3ae32fd2e2f798e8215865a8950d19df8330843608d4ee44e9f86849029724a";
+if (forwardedIdHex === expectedForwardedId) {
+ console.log("✅ Forwarded ID matches expected value.");
+} else {
+ console.error("❌ Forwarded ID does not match expected value.");
+}
+```
+
+> Note: `@noble/hashes` and `polkadot-api` are required dependencies for this code to work.
+
+## Failure Event Handling
+
+When an XCM fails, the transaction rolls back and no explicit failure event is emitted on-chain. The following are some ways to check for XCM failure:
+
+1. **View nested dispatch errors via indexers**: Most indexers display nested errors indicating why an XCM failed, e.g.:
+
+
+
+"error": {
+ "type": "Module",
+ "value": {
+ "type": "PolkadotXcm",
+ "value": {
+ "type": "LocalExecutionIncompleteWithError",
+ "value": {
+ "index": 0,
+ "error": {
+ "type": "FailedToTransactAsset"
+ }
+ }
+ }
+ }
+}
+
+
+
+ Common errors include missing assets, exceeded execution limits, or invalid asset locations. This nested error reporting, introduced in runtimes from `stable2506` onward, usually suffices to diagnose typical issues.
+
+2. **Replay using Chopsticks for full logs**: For detailed troubleshooting, try the following steps:
+
+ - Replay the failing extrinsic with logging enabled.
+ - See exactly which instruction failed and why.
+ - View complete error chains like `FailedToTransactAsset` or `AssetNotFound`.
+
+ This approach is invaluable for multi-hop flows and complex custom XCMs.
+
+## Recommended Debugging Workflow
+
+Follow these steps when debugging XCM failures:
+
+1. Start with indexer or API error output.
+2. If unclear, use Chopsticks to replay the exact transaction.
+3. Inspect logs for the failing XCM instruction and reason.
+4. Adjust weight limits, asset locations, or message construction as needed.
+
+!!! tip
+ See [Replay and Dry Run XCMs Using Chopsticks](/tutorials/interoperability/replay-and-dry-run-xcms/){target=\_blank} for replay instructions.
+
+## Best Practices
+
+- When manually setting `SetTopic`, always place it as the final instruction in your XCM to ensure it is respected by the runtime.
+- Prefer automatic topic insertion via high-level extrinsics for simplicity.
+- If your use case involves multi-hop or custom XCMs, manually set `SetTopic` (including remote XCM calls) to guarantee consistent tracing.
+- Ensure your `message_id` values are unique if you require deduplication or strict correlation.
+- When supporting legacy runtimes, be aware of the `forward_id_for` pattern.
+--- END CONTENT ---
+
Doc-Content: https://docs.polkadot.com/develop/interoperability/xcm-runtime-apis/
--- BEGIN CONTENT ---
---
@@ -30866,6 +31061,1093 @@ This approach provides accurate fee estimation for XCM teleports from Asset Hub
The key insight is understanding how asset references change based on the perspective of each chain in the XCM ecosystem, which is crucial for proper fee estimation and XCM construction.
--- END CONTENT ---
+Doc-Content: https://docs.polkadot.com/tutorials/interoperability/xcm-observability-in-action/
+--- BEGIN CONTENT ---
+---
+title: XCM Observability in Action
+description: Follow this step-by-step guide to trace, correlate, and debug cross-chain XCMs using observability tools in the Polkadot SDK.
+---
+
+# XCM Observability in Action
+
+## Introduction
+
+Cross-Consensus Messaging (XCM) enables interoperability within the Polkadot ecosystem; however, tracing flows across multiple chains is challenging in practice.
+
+Follow this tutorial to send assets between parachains and trace the resulting XCM across the origin and destination chains. By completing this tutorial, you will:
+
+- Capture `message_id` and [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank} for tracking.
+- Correlate [`PolkadotXcm.Sent`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank} and [`MessageQueue.Processed`](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank} events across chains.
+- Apply manual topic tagging for custom multi-hop flows.
+
+## Prerequisites
+
+Before you begin, ensure you have the following:
+
+- [Node.js and npm installed](https://nodejs.org/en/download){target=\_blank}.
+- [Chopsticks](/develop/toolkit/parachains/fork-chains/chopsticks/get-started/){target=\_blank} installed.
+- Access to local or remote parachain endpoints.
+- An origin chain running runtime `stable2503-5` or later.
+- A TypeScript development environment with essential tools.
+- Familiarity with [replaying or dry-running XCMs](/tutorials/interoperability/replay-and-dry-run-xcms/){target=\_blank}.
+
+## Set Up Your Workspace
+
+1. Run the following command to create a new project directory:
+
+ ```bash
+ mkdir -p xcm-obs-demo && cd xcm-obs-demo
+ ```
+
+2. Install Chopsticks globally using the command:
+
+ ```bash
+ npm install -g @acala-network/chopsticks@latest
+ ```
+
+3. Next, use the following command to download the 1.6.0 runtime, which is built from `stable2503-5` or later:
+
+ ```bash
+ mkdir -p wasms
+ wget https://github.com/polkadot-fellows/runtimes/releases/download/v1.6.0/asset-hub-polkadot_runtime-v1006000.compact.compressed.wasm -O wasms/asset-hub-polkadot_v1.6.0.wasm
+ ```
+
+4. Download the config file for Polkadot Hub:
+
+ ```bash
+ mkdir -p configs
+ wget https://raw.githubusercontent.com/AcalaNetwork/chopsticks/master/configs/polkadot-asset-hub.yml -O configs/polkadot-hub-override.yaml
+ ```
+
+5. Edit `configs/polkadot-hub-override.yaml` to include:
+
+ ```yaml title="configs/polkadot-hub-override.yaml"
+ ...
+ db: ./db.sqlite
+ wasm-override: wasms/asset-hub-polkadot_v1.6.0.wasm
+
+ import-storage:
+ ...
+ ```
+
+6. Use the following command to fork the relevant chains locally using Chopsticks:
+
+ ```bash
+ npx @acala-network/chopsticks xcm -r polkadot -p configs/polkadot-hub-override.yaml -p hydradx
+ ```
+
+ !!! tip
+
+ See the [Fork a Chain with Chopsticks](/tutorials/polkadot-sdk/testing/fork-live-chains/){target=\_blank} guide for detailed instructions.
+
+7. Open a new terminal in the same folder and initialize a Node.js project:
+
+ ```bash
+ npm init -y && npm pkg set type="module"
+ ```
+
+8. Install TypeScript and Polkadot dependencies:
+
+ ```bash
+ npm install --save-dev typescript @types/node tsx
+ npm install polkadot-api @polkadot-labs/hdkd @polkadot-labs/hdkd-helpers
+ npm install @noble/hashes
+ ```
+
+9. Initialize TypeScript using the command:
+
+ ```bash
+ npx tsc --init
+ ```
+
+10. Add descriptors:
+
+ ```bash
+ npx papi add assetHub -w ws://localhost:8000
+ npx papi add hydration -w ws://localhost:8001
+ ```
+
+## XCM Flow with Implicit `SetTopic`
+
+Assume the following values for this scenario:
+
+- **Origin**: Polkadot Hub
+- **Destination**: Hydration
+- **Extrinsic**: `limited_reserve_transfer_assets` (high-level)
+- **Topic**: Set automatically by the runtime
+
+Follow these steps to complete the implicit `SetTopic` flow:
+
+1. Create a file named `limited-reserve-transfer-assets.ts` and add the following code:
+
+ ```ts
+ import {Binary, createClient, Enum, type BlockInfo, type PolkadotClient} from "polkadot-api";
+import {withPolkadotSdkCompat} from "polkadot-api/polkadot-sdk-compat";
+import {getPolkadotSigner} from "polkadot-api/signer";
+import {getWsProvider} from "polkadot-api/ws-provider/web";
+import {
+ assetHub,
+ hydration,
+ XcmV3MultiassetFungibility,
+ XcmV3WeightLimit,
+ XcmV5Junction,
+ XcmV5Junctions,
+ XcmVersionedAssets,
+ XcmVersionedLocation,
+} from "@polkadot-api/descriptors";
+import {sr25519CreateDerive} from "@polkadot-labs/hdkd";
+import {
+ DEV_PHRASE,
+ entropyToMiniSecret,
+ mnemonicToEntropy,
+ ss58Address,
+} from "@polkadot-labs/hdkd-helpers";
+
+const XCM_VERSION = 5;
+const MAX_RETRIES = 8; // Number of attempts to wait for block finalisation
+
+const toHuman = (_key: any, value: any) => {
+ if (typeof value === "bigint") {
+ return Number(value);
+ }
+
+ if (value && typeof value === "object" && typeof value.asHex === "function") {
+ return value.asHex();
+ }
+
+ return value;
+};
+
+async function assertProcessedMessageId(
+ client: PolkadotClient,
+ api: any,
+ name: String,
+ blockBefore: BlockInfo,
+ expectedMessageId: String,
+) {
+ let processedMessageId = undefined;
+ for (let i = 0; i < MAX_RETRIES; i++) {
+ const blockAfter = await client.getFinalizedBlock();
+ if (blockAfter.number == blockBefore.number) {
+ const waiting = 1_000 * (2 ** i);
+ console.log(`⏳ Waiting ${waiting / 1_000}s for ${name} block to be finalised (${i + 1}/${MAX_RETRIES})...`);
+ await new Promise((resolve) => setTimeout(resolve, waiting));
+ continue;
+ }
+
+ console.log(`📦 Finalised on ${name} in block #${blockAfter.number}: ${blockAfter.hash}`);
+ const processedEvents = await api.event.MessageQueue.Processed.pull();
+ const processingFailedEvents = await api.event.MessageQueue.ProcessingFailed.pull();
+ if (processedEvents.length > 0) {
+ processedMessageId = processedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message processed on ${name}: ${processedMessageId}`);
+ break;
+ } else if (processingFailedEvents.length > 0) {
+ processedMessageId = processingFailedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message ProcessingFailed on ${name}: ${processedMessageId}`);
+ break;
+ } else {
+ console.log(`📣 No Processed events on ${name} found.`);
+ blockBefore = blockAfter; // Update the block before to the latest one
+ }
+ }
+
+ if (processedMessageId === expectedMessageId) {
+ console.log(`✅ Processed Message ID on ${name} matched.`);
+ } else if (processedMessageId === undefined) {
+ console.error(`❌ Processed Message ID on ${name} is undefined. Try increasing MAX_RETRIES to wait for block finalisation.`);
+ } else {
+ console.error(`❌ Processed Message ID [${processedMessageId}] on ${name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+}
+
+async function main() {
+ const para1Name = "Polkadot Hub";
+ const para1Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8000")),
+ );
+ const para1Api = para1Client.getTypedApi(assetHub);
+
+ const para2Name = "Hydration";
+ const para2Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8001")),
+ );
+ const para2Api = para2Client.getTypedApi(hydration);
+
+ const entropy = mnemonicToEntropy(DEV_PHRASE);
+ const miniSecret = entropyToMiniSecret(entropy);
+ const derive = sr25519CreateDerive(miniSecret);
+ const alice = derive("//Alice");
+ const alicePublicKey = alice.publicKey;
+ const aliceSigner = getPolkadotSigner(alicePublicKey, "Sr25519", alice.sign);
+ const aliceAddress = ss58Address(alicePublicKey);
+
+ const origin = Enum("system", Enum("Signed", aliceAddress));
+ const beneficiary = {
+ parents: 0,
+ interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
+ id: Binary.fromHex("0x9818ff3c27d256631065ecabf0c50e02551e5c5342b8669486c1e566fcbf847f")
+ })),
+ }
+
+ const tx: any = para1Api.tx.PolkadotXcm.limited_reserve_transfer_assets({
+ dest: XcmVersionedLocation.V5({
+ parents: 1,
+ interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(2034)),
+ }),
+ beneficiary: XcmVersionedLocation.V5(beneficiary),
+ assets: XcmVersionedAssets.V5([
+ {
+ id: {
+ parents: 0,
+ interior: XcmV5Junctions.X2([
+ XcmV5Junction.PalletInstance(50),
+ XcmV5Junction.GeneralIndex(1984n),
+ ]),
+ },
+ fun: XcmV3MultiassetFungibility.Fungible(500_000_000n),
+ },
+ ]),
+ fee_asset_item: 0,
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ });
+ const decodedCall: any = tx.decodedCall;
+ console.log("👀 Executing XCM:", JSON.stringify(decodedCall, toHuman, 2));
+
+ try {
+ const dryRunResult: any = await para1Api.apis.DryRunApi.dry_run_call(
+ origin,
+ decodedCall,
+ XCM_VERSION,
+ );
+ console.log("📦 Dry run result:", JSON.stringify(dryRunResult.value, toHuman, 2));
+
+ const executionResult = dryRunResult.value.execution_result;
+ if (!dryRunResult.success || !executionResult.success) {
+ console.error("❌ Local dry run failed!");
+ } else {
+ console.log("✅ Local dry run successful.");
+
+ const emittedEvents: [any] = dryRunResult.value.emitted_events;
+ const polkadotXcmSentEvent = emittedEvents.find(event =>
+ event.type === "PolkadotXcm" && event.value.type === "Sent"
+ );
+ if (polkadotXcmSentEvent === undefined) {
+ console.log(`⚠️ PolkadotXcm.Sent is only available in runtimes built from stable2503-5 or later.`);
+ } else {
+ let para2BlockBefore = await para2Client.getFinalizedBlock();
+ const extrinsic = await tx.signAndSubmit(aliceSigner);
+ const para1Block = extrinsic.block;
+ console.log(`📦 Finalised on ${para1Name} in block #${para1Block.number}: ${para1Block.hash}`);
+
+ if (!extrinsic.ok) {
+ const dispatchError = extrinsic.dispatchError;
+ if (dispatchError.type === "Module") {
+ const modErr: any = dispatchError.value;
+ console.error(`❌ Dispatch error in module: ${modErr.type} → ${modErr.value?.type}`);
+ } else {
+ console.error("❌ Dispatch error:", JSON.stringify(dispatchError, toHuman, 2));
+ }
+ }
+
+ const sentEvents: any = await para1Api.event.PolkadotXcm.Sent.pull();
+ if (sentEvents.length > 0) {
+ const sentMessageId = sentEvents[0].payload.message_id.asHex();
+ console.log(`📣 Last message sent on ${para1Name}: ${sentMessageId}`);
+ await assertProcessedMessageId(para2Client, para2Api, para2Name, para2BlockBefore, sentMessageId);
+ } else {
+ console.log(`📣 No Sent events on ${para1Name} found.`);
+ }
+ }
+ }
+ } finally {
+ para1Client.destroy();
+ para2Client.destroy();
+ }
+}
+
+main().catch(console.error);
+ ```
+
+2. Run your script with the following command:
+
+ ```bash
+ npx tsx limited-reserve-transfer-assets.ts
+ ```
+
+3. You will see terminal output similar to the following:
+
+
+ npx tsx limited-reserve-transfer-assets.ts
+ ✅ Local dry run successful.
+ 📦 Finalised on Polkadot Hub in block #9477291: 0xf54cecc017762c714bbdf3e82d72ed90886257ca17d32ec6dc8ea20e28110af8
+ 📣 Last message sent on Polkadot Hub: 0x20432393771dc049cea4900565a936d169b8ebdd64efa351890766df918615a4
+ 📦 Finalised on Hydration in block #8761211: 0xa4c493ba9328f38174aa7a9ade0779654839e9d3c83b2bafc60d4e5b7de6a00f
+ 📣 Last message processed on Hydration: 0x20432393771dc049cea4900565a936d169b8ebdd64efa351890766df918615a4
+ ✅ Processed Message ID on Hydration matched.
+
+
+### Forwarded XCM Example
+
+The following example illustrates the runtime adding a `SetTopic` to the forwarded XCM automatically:
+
+
+
+[
+ {
+ "type": "ReserveAssetDeposited",
+ "value": [...]
+ },
+ {
+ "type": "ClearOrigin"
+ },
+ {
+ "type": "BuyExecution",
+ "value": {...}
+ },
+ {
+ "type": "DepositAsset",
+ "value": {...}
+ },
+ {
+ "type": "SetTopic",
+ "value": "0x3b5650b78230aebb8f2573d2c5e8356494ab01e39e716087c177bf871dce70b9"
+ }
+]
+
+
+
+### Trace Events
+
+| Chain | Event | Field | Notes |
+|--------------|--------------------------|--------------|----------------------------------------|
+| Polkadot Hub | `PolkadotXcm.Sent` | `message_id` | Matches the topic in the forwarded XCM |
+| Hydration | `MessageQueue.Processed` | `id` | Matches origin's `message_id` |
+
+!!! note
+ Dry run-generated topics may differ from actual execution.
+
+## XCM Flow with Manual `SetTopic`
+
+Assume the following values for this scenario:
+
+- **Origin**: Polkadot Hub
+- **Destination**: Hydration
+- **Topic**: Manually assigned
+- **Goal**: Ensure traceability in custom multi-hop flows
+
+Follow these steps to complete the manual `SetTopic` flow:
+
+1. Create a new file named `deposit-reserve-asset-with-set-topic.ts` and add the following code:
+
+ ```ts
+ import {Binary, createClient, Enum, type BlockInfo, type PolkadotClient} from "polkadot-api";
+import {withPolkadotSdkCompat} from "polkadot-api/polkadot-sdk-compat";
+import {getPolkadotSigner} from "polkadot-api/signer";
+import {getWsProvider} from "polkadot-api/ws-provider/web";
+import {
+ assetHub,
+ hydration,
+ XcmV3MultiassetFungibility,
+ XcmV3WeightLimit,
+ XcmV5AssetFilter,
+ XcmV5Instruction,
+ XcmV5Junction,
+ XcmV5Junctions,
+ XcmV5WildAsset,
+ XcmVersionedXcm,
+} from "@polkadot-api/descriptors";
+import {sr25519CreateDerive} from "@polkadot-labs/hdkd";
+import {
+ DEV_PHRASE,
+ entropyToMiniSecret,
+ mnemonicToEntropy,
+ ss58Address,
+} from "@polkadot-labs/hdkd-helpers";
+
+const XCM_VERSION = 5;
+const MAX_RETRIES = 8; // Number of attempts to wait for block finalisation
+
+const toHuman = (_key: any, value: any) => {
+ if (typeof value === "bigint") {
+ return Number(value);
+ }
+
+ if (value && typeof value === "object" && typeof value.asHex === "function") {
+ return value.asHex();
+ }
+
+ return value;
+};
+
+async function assertProcessedMessageId(
+ client: PolkadotClient,
+ api: any,
+ name: String,
+ blockBefore: BlockInfo,
+ expectedMessageId: String,
+) {
+ let processedMessageId = undefined;
+ for (let i = 0; i < MAX_RETRIES; i++) {
+ const blockAfter = await client.getFinalizedBlock();
+ if (blockAfter.number == blockBefore.number) {
+ const waiting = 1_000 * (2 ** i);
+ console.log(`⏳ Waiting ${waiting / 1_000}s for ${name} block to be finalised (${i + 1}/${MAX_RETRIES})...`);
+ await new Promise((resolve) => setTimeout(resolve, waiting));
+ continue;
+ }
+
+ console.log(`📦 Finalised on ${name} in block #${blockAfter.number}: ${blockAfter.hash}`);
+ const processedEvents = await api.event.MessageQueue.Processed.pull();
+ const processingFailedEvents = await api.event.MessageQueue.ProcessingFailed.pull();
+ if (processedEvents.length > 0) {
+ processedMessageId = processedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message processed on ${name}: ${processedMessageId}`);
+ break;
+ } else if (processingFailedEvents.length > 0) {
+ processedMessageId = processingFailedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message ProcessingFailed on ${name}: ${processedMessageId}`);
+ break;
+ } else {
+ console.log(`📣 No Processed events on ${name} found.`);
+ blockBefore = blockAfter; // Update the block before to the latest one
+ }
+ }
+
+ if (processedMessageId === expectedMessageId) {
+ console.log(`✅ Processed Message ID on ${name} matched.`);
+ } else if (processedMessageId === undefined) {
+ console.error(`❌ Processed Message ID on ${name} is undefined. Try increasing MAX_RETRIES to wait for block finalisation.`);
+ } else {
+ console.error(`❌ Processed Message ID [${processedMessageId}] on ${name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+}
+
+async function main() {
+ const para1Name = "Polkadot Hub";
+ const para1Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8000")),
+ );
+ const para1Api = para1Client.getTypedApi(assetHub);
+
+ const para2Name = "Hydration";
+ const para2Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8001")),
+ );
+ const para2Api = para2Client.getTypedApi(hydration);
+
+ const entropy = mnemonicToEntropy(DEV_PHRASE);
+ const miniSecret = entropyToMiniSecret(entropy);
+ const derive = sr25519CreateDerive(miniSecret);
+ const alice = derive("//Alice");
+ const alicePublicKey = alice.publicKey;
+ const aliceSigner = getPolkadotSigner(alicePublicKey, "Sr25519", alice.sign);
+ const aliceAddress = ss58Address(alicePublicKey);
+
+ const origin = Enum("system", Enum("Signed", aliceAddress));
+ const beneficiary = {
+ parents: 0,
+ interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
+ id: Binary.fromHex("0x9818ff3c27d256631065ecabf0c50e02551e5c5342b8669486c1e566fcbf847f")
+ })),
+ }
+ const expectedMessageId = "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2";
+
+ const message = XcmVersionedXcm.V5([
+ XcmV5Instruction.WithdrawAsset([
+ {
+ id: {
+ parents: 1,
+ interior: XcmV5Junctions.Here(),
+ },
+ fun: XcmV3MultiassetFungibility.Fungible(1_000_000_000n),
+ },
+ ]),
+
+ XcmV5Instruction.ClearOrigin(),
+
+ XcmV5Instruction.BuyExecution({
+ fees: {
+ id: {
+ parents: 1,
+ interior: XcmV5Junctions.Here(),
+ },
+ fun: XcmV3MultiassetFungibility.Fungible(500_000_000n),
+ },
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ }),
+
+ XcmV5Instruction.DepositReserveAsset({
+ assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.All()),
+ dest: {
+ parents: 1,
+ interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(2034)),
+ },
+ xcm: [
+ XcmV5Instruction.BuyExecution({
+ fees: {
+ id: {
+ parents: 1,
+ interior: XcmV5Junctions.Here(),
+ },
+ fun: XcmV3MultiassetFungibility.Fungible(500_000_000n),
+ },
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ }),
+
+ XcmV5Instruction.DepositAsset({
+ assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.All()),
+ beneficiary,
+ }),
+ ],
+ }),
+
+ XcmV5Instruction.SetTopic(Binary.fromHex(expectedMessageId)),
+ ]);
+
+ const weight: any = await para1Api.apis.XcmPaymentApi.query_xcm_weight(message);
+ if (weight.success !== true) {
+ console.error("❌ Failed to query XCM weight:", weight.error);
+ para1Client.destroy();
+ return;
+ }
+
+ const tx: any = para1Api.tx.PolkadotXcm.execute({
+ message,
+ max_weight: weight.value,
+ });
+ const decodedCall: any = tx.decodedCall;
+ console.log("👀 Executing XCM:", JSON.stringify(decodedCall, toHuman, 2));
+
+ try {
+ const dryRunResult: any = await para1Api.apis.DryRunApi.dry_run_call(
+ origin,
+ decodedCall,
+ XCM_VERSION,
+ );
+ console.log("📦 Dry run result:", JSON.stringify(dryRunResult.value, toHuman, 2));
+
+ const executionResult = dryRunResult.value.execution_result;
+ if (!dryRunResult.success || !executionResult.success) {
+ console.error("❌ Local dry run failed!");
+ } else {
+ console.log("✅ Local dry run successful.");
+
+ const emittedEvents: [any] = dryRunResult.value.emitted_events;
+ const polkadotXcmSentEvent = emittedEvents.find(event =>
+ event.type === "PolkadotXcm" && event.value.type === "Sent"
+ );
+ if (polkadotXcmSentEvent === undefined) {
+ console.log(`⚠️ PolkadotXcm.Sent is only available in runtimes built from stable2503-5 or later.`);
+ } else {
+ let para2BlockBefore = await para2Client.getFinalizedBlock();
+ const extrinsic = await tx.signAndSubmit(aliceSigner);
+ const para1Block = extrinsic.block;
+ console.log(`📦 Finalised on ${para1Name} in block #${para1Block.number}: ${para1Block.hash}`);
+
+ if (!extrinsic.ok) {
+ const dispatchError = extrinsic.dispatchError;
+ if (dispatchError.type === "Module") {
+ const modErr: any = dispatchError.value;
+ console.error(`❌ Dispatch error in module: ${modErr.type} → ${modErr.value?.type}`);
+ } else {
+ console.error("❌ Dispatch error:", JSON.stringify(dispatchError, toHuman, 2));
+ }
+ }
+
+ const sentEvents: any = await para1Api.event.PolkadotXcm.Sent.pull();
+ if (sentEvents.length > 0) {
+ const sentMessageId = sentEvents[0].payload.message_id.asHex();
+ console.log(`📣 Last message sent on ${para1Name}: ${sentMessageId}`);
+ if (sentMessageId === expectedMessageId) {
+ console.log(`✅ Sent Message ID on ${para1Name} matched.`);
+ } else {
+ console.error(`❌ Sent Message ID [${sentMessageId}] on ${para1Name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+ await assertProcessedMessageId(para2Client, para2Api, para2Name, para2BlockBefore, expectedMessageId);
+ } else {
+ console.log(`📣 No Sent events on ${para1Name} found.`);
+ }
+ }
+ }
+ } finally {
+ para1Client.destroy();
+ para2Client.destroy();
+ }
+}
+
+main().catch(console.error);
+ ```
+
+2. Run your script with the following command:
+
+ ```bash
+ npx tsx deposit-reserve-asset-with-set-topic.ts
+ ```
+
+3. You will see terminal output similar to the following:
+
+
+ npx tsx limited-reserve-transfer-assets.ts
+ ✅ Local dry run successful.
+ 📦 Finalised on Polkadot Hub in block #9471830: 0x98bd858739b3b5dd558def60cbd85d5e7fb2f4e33b0c00e1895e316541d727d9
+ 📣 Last message sent on Polkadot Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Sent Message ID on Polkadot Hub matched.
+ 📦 Finalised on Hydration in block #8749233: 0xe1413c5126698d7189d6f55a38e62d07ea4915078c2b1f3914d70f670e79e162
+ 📣 Last message processed on Hydration: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Processed Message ID on Hydration matched.
+
+
+### Forwarded XCM Example
+
+The following example illustrates that the runtime preserves the manual `SetTopic` value:
+
+
+
+[
+ {
+ "type": "ReserveAssetDeposited",
+ "value": [...]
+ },
+ {
+ "type": "ClearOrigin"
+ },
+ {
+ "type": "BuyExecution",
+ "value": {...}
+ },
+ {
+ "type": "DepositAsset",
+ "value": {...}
+ },
+ {
+ "type": "SetTopic",
+ "value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2"
+ }
+]
+
+
+
+## Multi-hop XCM Transfer with Manual `SetTopic`
+
+Assume the following values for this scenario:
+
+- **Origin**: Polkadot Hub
+- **Destination**: Hydration
+- **Topic**: Manually assigned and preserved over multiple hops (including remote XCMs)
+- **Goal**: Enable consistent tracing across multi-hop XCM flows
+
+Follow these steps to complete the multi-hop manual `SetTopic` flow:
+
+1. Create a new file named `initiate-reserve-withdraw-with-set-topic.ts` and add the following code:
+
+ ```ts
+ import {Binary, createClient, Enum, type BlockInfo, type PolkadotClient} from "polkadot-api";
+import {withPolkadotSdkCompat} from "polkadot-api/polkadot-sdk-compat";
+import {getPolkadotSigner} from "polkadot-api/signer";
+import {getWsProvider} from "polkadot-api/ws-provider/web";
+import {
+ assetHub,
+ hydration,
+ XcmV2MultiassetWildFungibility,
+ XcmV3MultiassetFungibility,
+ XcmV3WeightLimit,
+ XcmV5AssetFilter,
+ XcmV5Instruction,
+ XcmV5Junction,
+ XcmV5Junctions,
+ XcmV5WildAsset,
+ XcmVersionedXcm,
+} from "@polkadot-api/descriptors";
+import {sr25519CreateDerive} from "@polkadot-labs/hdkd";
+import {
+ DEV_PHRASE,
+ entropyToMiniSecret,
+ mnemonicToEntropy,
+ ss58Address,
+} from "@polkadot-labs/hdkd-helpers";
+
+const XCM_VERSION = 5;
+const MAX_RETRIES = 8; // Number of attempts to wait for block finalisation
+
+const toHuman = (_key: any, value: any) => {
+ if (typeof value === "bigint") {
+ return Number(value);
+ }
+
+ if (value && typeof value === "object" && typeof value.asHex === "function") {
+ return value.asHex();
+ }
+
+ return value;
+};
+
+async function assertProcessedMessageId(
+ client: PolkadotClient,
+ api: any,
+ name: String,
+ blockBefore: BlockInfo,
+ expectedMessageId: String,
+) {
+ let processedMessageId = undefined;
+ for (let i = 0; i < MAX_RETRIES; i++) {
+ const blockAfter = await client.getFinalizedBlock();
+ if (blockAfter.number == blockBefore.number) {
+ const waiting = 1_000 * (2 ** i);
+ console.log(`⏳ Waiting ${waiting / 1_000}s for ${name} block to be finalised (${i + 1}/${MAX_RETRIES})...`);
+ await new Promise((resolve) => setTimeout(resolve, waiting));
+ continue;
+ }
+
+ console.log(`📦 Finalised on ${name} in block #${blockAfter.number}: ${blockAfter.hash}`);
+ const processedEvents = await api.event.MessageQueue.Processed.pull();
+ const processingFailedEvents = await api.event.MessageQueue.ProcessingFailed.pull();
+ if (processedEvents.length > 0) {
+ processedMessageId = processedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message processed on ${name}: ${processedMessageId}`);
+ break;
+ } else if (processingFailedEvents.length > 0) {
+ processedMessageId = processingFailedEvents[0].payload.id.asHex();
+ console.log(`📣 Last message ProcessingFailed on ${name}: ${processedMessageId}`);
+ break;
+ } else {
+ console.log(`📣 No Processed events on ${name} found.`);
+ blockBefore = blockAfter; // Update the block before to the latest one
+ }
+ }
+
+ if (processedMessageId === expectedMessageId) {
+ console.log(`✅ Processed Message ID on ${name} matched.`);
+ } else if (processedMessageId === undefined) {
+ console.error(`❌ Processed Message ID on ${name} is undefined. Try increasing MAX_RETRIES to wait for block finalisation.`);
+ } else {
+ console.error(`❌ Processed Message ID [${processedMessageId}] on ${name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+}
+
+async function main() {
+ const para1Name = "Polkadot Hub";
+ const para1Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8000")),
+ );
+ const para1Api = para1Client.getTypedApi(assetHub);
+
+ const para2Name = "Hydration";
+ const para2Client = createClient(
+ withPolkadotSdkCompat(getWsProvider("ws://localhost:8001")),
+ );
+ const para2Api = para2Client.getTypedApi(hydration);
+
+ const entropy = mnemonicToEntropy(DEV_PHRASE);
+ const miniSecret = entropyToMiniSecret(entropy);
+ const derive = sr25519CreateDerive(miniSecret);
+ const alice = derive("//Alice");
+ const alicePublicKey = alice.publicKey;
+ const aliceSigner = getPolkadotSigner(alicePublicKey, "Sr25519", alice.sign);
+ const aliceAddress = ss58Address(alicePublicKey);
+
+ const origin = Enum("system", Enum("Signed", aliceAddress));
+ const beneficiary = {
+ parents: 0,
+ interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
+ id: Binary.fromHex("0x9818ff3c27d256631065ecabf0c50e02551e5c5342b8669486c1e566fcbf847f")
+ })),
+ }
+ const tokenId = XcmV5Junction.GeneralIndex(1337n); // Change to another token if FailedToTransactAsset("Funds are unavailable")
+ const assetId = {
+ parents: 0,
+ interior: XcmV5Junctions.X2([
+ XcmV5Junction.PalletInstance(50),
+ tokenId,
+ ]),
+ };
+ const giveId = {
+ parents: 1,
+ interior: XcmV5Junctions.X3([
+ XcmV5Junction.Parachain(1000),
+ XcmV5Junction.PalletInstance(50),
+ tokenId,
+ ]),
+ };
+ const giveFun = XcmV3MultiassetFungibility.Fungible(1_000_000n);
+ const dest = {
+ parents: 1,
+ interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(2034)),
+ };
+ const wantId = {
+ parents: 1,
+ interior: XcmV5Junctions.Here(),
+ };
+ const wantFun = XcmV3MultiassetFungibility.Fungible(2_000_000_000n); // Adjust the exchange rate if xcm_error is NoDeal
+ const expectedMessageId = "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2";
+
+ const message = XcmVersionedXcm.V5([
+ XcmV5Instruction.WithdrawAsset([{
+ id: assetId,
+ fun: giveFun,
+ }]),
+
+ XcmV5Instruction.SetFeesMode({jit_withdraw: true}),
+
+ XcmV5Instruction.DepositReserveAsset({
+ assets: XcmV5AssetFilter.Wild(
+ XcmV5WildAsset.AllOf({
+ id: assetId,
+ fun: XcmV2MultiassetWildFungibility.Fungible(),
+ })),
+ dest,
+ xcm: [
+ XcmV5Instruction.BuyExecution({
+ fees: {
+ id: giveId,
+ fun: giveFun,
+ },
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ }),
+
+ XcmV5Instruction.ExchangeAsset({
+ give: XcmV5AssetFilter.Wild(
+ XcmV5WildAsset.AllOf({
+ id: giveId,
+ fun: XcmV2MultiassetWildFungibility.Fungible(),
+ }),
+ ),
+ want: [{
+ id: wantId,
+ fun: wantFun,
+ }],
+ maximal: false,
+ }),
+
+ XcmV5Instruction.InitiateReserveWithdraw({
+ assets: XcmV5AssetFilter.Wild(
+ XcmV5WildAsset.AllOf({
+ id: wantId,
+ fun: XcmV2MultiassetWildFungibility.Fungible(),
+ }),
+ ),
+ reserve: {
+ parents: 1,
+ interior: XcmV5Junctions.X1(
+ XcmV5Junction.Parachain(1000),
+ ),
+ },
+ xcm: [
+ XcmV5Instruction.BuyExecution({
+ fees: {
+ id: wantId,
+ fun: wantFun,
+ },
+ weight_limit: XcmV3WeightLimit.Unlimited(),
+ }),
+
+ XcmV5Instruction.DepositAsset({
+ assets: XcmV5AssetFilter.Wild(
+ XcmV5WildAsset.AllOf({
+ id: wantId,
+ fun: XcmV2MultiassetWildFungibility.Fungible(),
+ }),
+ ),
+ beneficiary,
+ }),
+
+ XcmV5Instruction.SetTopic(Binary.fromHex(expectedMessageId)), // Ensure the same topic is also set on remote XCM calls
+ ],
+ }),
+ ],
+ }),
+
+ XcmV5Instruction.SetTopic(Binary.fromHex(expectedMessageId)),
+ ]);
+
+ const weight: any = await para1Api.apis.XcmPaymentApi.query_xcm_weight(message);
+ if (weight.success !== true) {
+ console.error("❌ Failed to query XCM weight:", weight.error);
+ para1Client.destroy();
+ return;
+ }
+
+ const tx: any = para1Api.tx.PolkadotXcm.execute({
+ message,
+ max_weight: weight.value,
+ });
+ const decodedCall: any = tx.decodedCall;
+ console.log("👀 Executing XCM:", JSON.stringify(decodedCall, toHuman, 2));
+
+ try {
+ const dryRunResult: any = await para1Api.apis.DryRunApi.dry_run_call(
+ origin,
+ decodedCall,
+ XCM_VERSION,
+ );
+ console.log("📦 Dry run result:", JSON.stringify(dryRunResult.value, toHuman, 2));
+
+ const executionResult = dryRunResult.value.execution_result;
+ if (!dryRunResult.success || !executionResult.success) {
+ console.error("❌ Local dry run failed!");
+ } else {
+ console.log("✅ Local dry run successful.");
+
+ const emittedEvents: [any] = dryRunResult.value.emitted_events;
+ const polkadotXcmSentEvent = emittedEvents.find(event =>
+ event.type === "PolkadotXcm" && event.value.type === "Sent"
+ );
+ if (polkadotXcmSentEvent === undefined) {
+ console.log(`⚠️ PolkadotXcm.Sent is only available in runtimes built from stable2503-5 or later.`);
+ } else {
+ let para2BlockBefore = await para2Client.getFinalizedBlock();
+ const extrinsic = await tx.signAndSubmit(aliceSigner);
+ const para1Block = extrinsic.block;
+ console.log(`📦 Finalised on ${para1Name} in block #${para1Block.number}: ${para1Block.hash}`);
+
+ if (!extrinsic.ok) {
+ const dispatchError = extrinsic.dispatchError;
+ if (dispatchError.type === "Module") {
+ const modErr: any = dispatchError.value;
+ console.error(`❌ Dispatch error in module: ${modErr.type} → ${modErr.value?.type}`);
+ } else {
+ console.error("❌ Dispatch error:", JSON.stringify(dispatchError, toHuman, 2));
+ }
+ }
+
+ const sentEvents: any = await para1Api.event.PolkadotXcm.Sent.pull();
+ if (sentEvents.length > 0) {
+ const sentMessageId = sentEvents[0].payload.message_id.asHex();
+ console.log(`📣 Last message sent on ${para1Name}: ${sentMessageId}`);
+ if (sentMessageId === expectedMessageId) {
+ console.log(`✅ Sent Message ID on ${para1Name} matched.`);
+ } else {
+ console.error(`❌ Sent Message ID [${sentMessageId}] on ${para1Name} doesn't match expected Message ID [${expectedMessageId}].`);
+ }
+ await assertProcessedMessageId(para2Client, para2Api, para2Name, para2BlockBefore, expectedMessageId);
+ await assertProcessedMessageId(para1Client, para1Api, para1Name, para1Block, expectedMessageId);
+ } else {
+ console.log(`📣 No Sent events on ${para1Name} found.`);
+ }
+ }
+ }
+ } finally {
+ para1Client.destroy();
+ para2Client.destroy();
+ }
+}
+
+main().catch(console.error);
+ ```
+
+2. Run your script with the following command:
+
+ ```bash
+ npx tsx initiate-reserve-withdraw-with-set-topic.ts
+ ```
+
+3. You will see terminal output similar to the following. Note, the same `message_id` is present in all relevant events across chains:
+
+
+ npx tsx initiate-reserve-withdraw-with-set-topic.ts
+ ✅ Local dry run successful.
+ 📦 Finalised on Polkadot Hub in block #9471831: 0x2620f7e29765fc953263b7835711011616702c9d82ef5306fe3ef4196cb75cab
+ 📣 Last message sent on Polkadot Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Sent Message ID on Polkadot Hub matched.
+ 📦 Finalised on Hydration in block #8749235: 0xafe7f6149b1773a8d3d229040cda414aafd64baaeffa37fb4a5b2a542308b2d6
+ 📣 Last message processed on Hydration: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Processed Message ID on Hydration matched.
+ 📦 Finalised on Polkadot Hub in block #9471832: 0x7c150b69e3562694f0573e4fee73dfb86f3ab71b808679a1777586ff24643e9a
+ 📣 Last message processed on Polkadot Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2
+ ✅ Processed Message ID on Polkadot Hub matched.
+
+
+### Forwarded XCM Example (Hydration)
+
+The following example illustrates how runtime preserves your `SetTopic` throughout the multi-hop flow:
+
+
+
+[
+ {
+ "type": "ReserveAssetDeposited",
+ "value": [...]
+ },
+ {
+ "type": "ClearOrigin"
+ },
+ {
+ "type": "BuyExecution",
+ "value": {...}
+ },
+ {
+ "type": "ExchangeAsset",
+ "value": {...}
+ },
+ {
+ "type": "InitiateReserveWithdraw",
+ "value": {
+ "assets": {...},
+ "reserve": {...},
+ "xcm": [
+ {
+ "type": "BuyExecution",
+ "value": {...}
+ },
+ {
+ "type": "DepositAsset",
+ "value": {...}
+ },
+ {
+ "type": "SetTopic",
+ "value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2"
+ }
+ ]
+ }
+ },
+ {
+ "type": "SetTopic",
+ "value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2"
+ }
+]
+
+
+
+## Script Troubleshooting Tips
+
+### Error: "Processed Message ID is `undefined`"
+
+This error usually means that the message has not yet been processed within the default retry window. If you see the following error when running a script:
+
+> ❌ Processed Message ID on Hydration is undefined. Try increasing MAX_RETRIES to wait for block finalisation.
+
+Update your script to increase the `MAX_RETRIES` value to give the chain more time:
+
+```ts
+const MAX_RETRIES = 8; // Number of attempts to wait for block finalisation
+```
+
+### Error: `PolkadotXcm.Sent` Event Not Found
+
+If you encounter an error indicating that `PolkadotXcm.Sent` is unavailable, like the following:
+
+> ⚠️ PolkadotXcm.Sent is only available in runtimes built from stable2503-5 or later.
+
+This error usually means that your runtime needs to be updated. Ensure that `wasm-override` is updated to runtime version 1.6.0+, or to any runtime built from `stable2503-5` or later.
+
+For details on updating your workspace, see [Setting Up Your Workspace](#setting-up-your-workspace).
+
+## Conclusion
+
+Congratulations! By completing this guide, you should now understand:
+
+- How `SetTopic` and `message_id` enable tracing and correlating XCMs across chains.
+- How to interpret and debug XCM failure cases.
+- How to manually and automatically manage topics for multi-hop flows.
+- How to use the legacy workaround for older runtimes with derived IDs.
+
+With these scenarios and debugging steps, you can confidently develop, trace, and troubleshoot XCM workflows across chains.
+
+## Additional Resources
+
+To learn more about XCM Observability features and best practices, see [XCM Observability](/develop/interoperability/xcm-observability){target=\_blank}.
+--- END CONTENT ---
+
Doc-Content: https://docs.polkadot.com/tutorials/interoperability/xcm-transfers/from-relaychain-to-parachain/
--- BEGIN CONTENT ---
---
diff --git a/llms.txt b/llms.txt
index 43a669e24..ca2c804c8 100644
--- a/llms.txt
+++ b/llms.txt
@@ -25,6 +25,7 @@
- [Transact](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-guides/from-apps/transact.md): Learn how to execute arbitrary calls on remote chains using the Transact instruction, enabling cross-chain function execution and remote pallet interactions.
- [Transfers](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-guides/from-apps/transfers.md): Learn how to perform cross-chain asset transfers using XCM, including teleport, reserve transfers, and handling different asset types across parachains.
- [XCM Guides](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-guides/index.md): Comprehensive guides for implementing XCM functionality in applications and understanding cross-chain interactions.
+- [XCM Observability](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-observability.md): A conceptual overview of XCM observability in Polkadot covers message correlation, tracing, and debugging features in modern runtimes.
- [XCM Runtime APIs](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/interoperability/xcm-runtime-apis.md): Learn about XCM Runtime APIs in Polkadot for cross-chain communication. Explore the APIs to simulate and test XCM messages before execution on the network.
- [Networks](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/networks.md): Explore the Polkadot ecosystem networks and learn the unique purposes of each, tailored for blockchain innovation, testing, and enterprise-grade solutions.
- [Add a Pallet to the Runtime](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/parachains/customize-parachain/add-existing-pallets.md): Learn how to include and configure pallets in a Polkadot SDK-based runtime, from adding dependencies to implementing necessary traits.
@@ -88,7 +89,7 @@
- [Storage](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/toolkit/integrations/storage.md): Explore decentralized storage solutions for your Polkadot dApp. Discover key integrations, such as Crust and IPFS, for robust, censorship-resistant data storage.
- [Transaction Construction](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/toolkit/integrations/transaction-construction.md): Understand how to construct, sign, and broadcast transactions in the Polkadot ecosystem using various tools and libraries.
- [Wallets](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/toolkit/integrations/wallets.md): Explore blockchain wallets. Securely manage digital assets with hot wallets for online access or cold wallets for offline, enhanced security.
-- [Interoperability](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/toolkit/interoperability/index.md): Explore Polkadot's XCM tooling ecosystem, featuring the Asset Transfer API and other utilities for implementing cross-chain messaging and transfers.
+- [Interoperability](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/toolkit/interoperability/index.md): Explore Polkadot's XCM tooling ecosystem, featuring utilities for implementing cross-chain messaging and transfers.
- [ParaSpell XCM SDK](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/toolkit/interoperability/paraspell-xcm-sdk/index.md): A powerful open-source library that simplifies XCM integration, enabling developers to easily build interoperable dApps on Polkadot.
- [XCM Tools](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/toolkit/interoperability/xcm-tools.md): Explore essential XCM tools across Polkadot, crafted to enhance cross-chain functionality and integration within the ecosystem.
- [E2E Testing on Polkadot SDK Chains](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/develop/toolkit/parachains/e2e-testing/index.md): Discover a suite of tools for E2E testing on Polkadot SDK-based blockchains, including configuration management, automation, and debugging utilities.
@@ -181,6 +182,7 @@
- [Opening HRMP Channels Between Parachains](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-channels/para-to-para.md): Learn how to open HRMP channels between parachains on Polkadot. Discover the step-by-step process for establishing uni- and bidirectional communication.
- [Opening HRMP Channels with System Parachains](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-channels/para-to-system.md): Learn how to open HRMP channels with Polkadot system parachains. Discover the process for establishing bi-directional communication using a single XCM message.
- [XCM Fee Estimation](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-fee-estimation.md): This tutorial demonstrates how to estimate the fees for teleporting assets from the Paseo Asset Hub parachain to the Paseo Bridge Hub chain.
+- [XCM Observability in Action](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-observability-in-action.md): Follow this step-by-step guide to trace, correlate, and debug cross-chain XCMs using observability tools in the Polkadot SDK.
- [XCM Transfers from Relay Chain to Parachain](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-transfers/from-relaychain-to-parachain.md): Learn how to perform a reserve-backed asset transfer between a relay chain and a parachain using XCM for cross-chain interoperability.
- [XCM Transfers](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/interoperability/xcm-transfers/index.md): Explore tutorials on performing transfers between different consensus systems using XCM technology to enable cross-chain interoperability.
- [Fast Track a Governance Proposal](https://raw.githubusercontent.com/polkadot-developers/polkadot-docs/refs/heads/master/tutorials/onchain-governance/fast-track-gov-proposal.md): Learn how to fast-track governance proposals in Polkadot's OpenGov using Chopsticks. Simulate, test, and execute proposals confidently.
diff --git a/tutorials/interoperability/.nav.yml b/tutorials/interoperability/.nav.yml
index c366ac17e..4c7096f87 100644
--- a/tutorials/interoperability/.nav.yml
+++ b/tutorials/interoperability/.nav.yml
@@ -5,3 +5,4 @@ nav:
- xcm-transfers
- 'Replay and Dry Run XCMs': replay-and-dry-run-xcms.md
- 'XCM Fee Estimation': xcm-fee-estimation.md
+ - 'XCM Observability in Action': xcm-observability-in-action.md
diff --git a/tutorials/interoperability/xcm-observability-in-action.md b/tutorials/interoperability/xcm-observability-in-action.md
new file mode 100644
index 000000000..c04d49299
--- /dev/null
+++ b/tutorials/interoperability/xcm-observability-in-action.md
@@ -0,0 +1,251 @@
+---
+title: XCM Observability in Action
+description: Follow this step-by-step guide to trace, correlate, and debug cross-chain XCMs using observability tools in the Polkadot SDK.
+---
+
+# XCM Observability in Action
+
+## Introduction
+
+Cross-Consensus Messaging (XCM) enables interoperability within the Polkadot ecosystem; however, tracing flows across multiple chains is challenging in practice.
+
+Follow this tutorial to send assets between parachains and trace the resulting XCM across the origin and destination chains. By completing this tutorial, you will:
+
+- Capture `message_id` and [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank} for tracking.
+- Correlate [`PolkadotXcm.Sent`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank} and [`MessageQueue.Processed`](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank} events across chains.
+- Apply manual topic tagging for custom multi-hop flows.
+
+## Prerequisites
+
+Before you begin, ensure you have the following:
+
+- [Node.js and npm installed](https://nodejs.org/en/download){target=\_blank}.
+- [Chopsticks](/develop/toolkit/parachains/fork-chains/chopsticks/get-started/){target=\_blank} installed.
+- Access to local or remote parachain endpoints.
+- An origin chain running runtime `stable2503-5` or later.
+- A TypeScript development environment with essential tools.
+- Familiarity with [replaying or dry-running XCMs](/tutorials/interoperability/replay-and-dry-run-xcms/){target=\_blank}.
+
+## Set Up Your Workspace
+
+1. Run the following command to create a new project directory:
+
+ ```bash
+ mkdir -p xcm-obs-demo && cd xcm-obs-demo
+ ```
+
+2. Install Chopsticks globally using the command:
+
+ ```bash
+ npm install -g @acala-network/chopsticks@latest
+ ```
+
+3. Next, use the following command to download the 1.6.0 runtime, which is built from `stable2503-5` or later:
+
+ ```bash
+ mkdir -p wasms
+ wget https://github.com/polkadot-fellows/runtimes/releases/download/v1.6.0/asset-hub-polkadot_runtime-v1006000.compact.compressed.wasm -O wasms/asset-hub-polkadot_v1.6.0.wasm
+ ```
+
+4. Download the config file for Polkadot Hub:
+
+ ```bash
+ mkdir -p configs
+ wget https://raw.githubusercontent.com/AcalaNetwork/chopsticks/master/configs/polkadot-asset-hub.yml -O configs/polkadot-hub-override.yaml
+ ```
+
+5. Edit `configs/polkadot-hub-override.yaml` to include:
+
+ ```yaml title="configs/polkadot-hub-override.yaml"
+ ...
+ db: ./db.sqlite
+ wasm-override: wasms/asset-hub-polkadot_v1.6.0.wasm
+
+ import-storage:
+ ...
+ ```
+
+6. Use the following command to fork the relevant chains locally using Chopsticks:
+
+ ```bash
+ npx @acala-network/chopsticks xcm -r polkadot -p configs/polkadot-hub-override.yaml -p hydradx
+ ```
+
+ !!! tip
+
+ See the [Fork a Chain with Chopsticks](/tutorials/polkadot-sdk/testing/fork-live-chains/){target=\_blank} guide for detailed instructions.
+
+7. Open a new terminal in the same folder and initialize a Node.js project:
+
+ ```bash
+ npm init -y && npm pkg set type="module"
+ ```
+
+8. Install TypeScript and Polkadot dependencies:
+
+ ```bash
+ npm install --save-dev typescript @types/node tsx
+ npm install polkadot-api @polkadot-labs/hdkd @polkadot-labs/hdkd-helpers
+ npm install @noble/hashes
+ ```
+
+9. Initialize TypeScript using the command:
+
+ ```bash
+ npx tsc --init
+ ```
+
+10. Add descriptors:
+
+ ```bash
+ npx papi add assetHub -w ws://localhost:8000
+ npx papi add hydration -w ws://localhost:8001
+ ```
+
+## XCM Flow with Implicit `SetTopic`
+
+Assume the following values for this scenario:
+
+- **Origin**: Polkadot Hub
+- **Destination**: Hydration
+- **Extrinsic**: `limited_reserve_transfer_assets` (high-level)
+- **Topic**: Set automatically by the runtime
+
+Follow these steps to complete the implicit `SetTopic` flow:
+
+1. Create a file named `limited-reserve-transfer-assets.ts` and add the following code:
+
+ ```ts
+ --8<-- 'code/tutorials/interoperability/xcm-observability-in-action/limited-reserve-transfer-assets.ts'
+ ```
+
+2. Run your script with the following command:
+
+ ```bash
+ npx tsx limited-reserve-transfer-assets.ts
+ ```
+
+3. You will see terminal output similar to the following:
+
+ --8<-- 'code/tutorials/interoperability/xcm-observability-in-action/limited-reserve-transfer-assets-result.html'
+
+### Forwarded XCM Example
+
+The following example illustrates the runtime adding a `SetTopic` to the forwarded XCM automatically:
+
+--8<-- 'code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm.html'
+
+### Trace Events
+
+| Chain | Event | Field | Notes |
+|--------------|--------------------------|--------------|----------------------------------------|
+| Polkadot Hub | `PolkadotXcm.Sent` | `message_id` | Matches the topic in the forwarded XCM |
+| Hydration | `MessageQueue.Processed` | `id` | Matches origin's `message_id` |
+
+!!! note
+ Dry run-generated topics may differ from actual execution.
+
+## XCM Flow with Manual `SetTopic`
+
+Assume the following values for this scenario:
+
+- **Origin**: Polkadot Hub
+- **Destination**: Hydration
+- **Topic**: Manually assigned
+- **Goal**: Ensure traceability in custom multi-hop flows
+
+Follow these steps to complete the manual `SetTopic` flow:
+
+1. Create a new file named `deposit-reserve-asset-with-set-topic.ts` and add the following code:
+
+ ```ts
+ --8<-- 'code/tutorials/interoperability/xcm-observability-in-action/deposit-reserve-asset-with-set-topic.ts'
+ ```
+
+2. Run your script with the following command:
+
+ ```bash
+ npx tsx deposit-reserve-asset-with-set-topic.ts
+ ```
+
+3. You will see terminal output similar to the following:
+
+ --8<-- 'code/tutorials/interoperability/xcm-observability-in-action/deposit-reserve-asset-with-set-topic-result.html'
+
+### Forwarded XCM Example
+
+The following example illustrates that the runtime preserves the manual `SetTopic` value:
+
+--8<-- 'code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm-custom-topic.html'
+
+## Multi-hop XCM Transfer with Manual `SetTopic`
+
+Assume the following values for this scenario:
+
+- **Origin**: Polkadot Hub
+- **Destination**: Hydration
+- **Topic**: Manually assigned and preserved over multiple hops (including remote XCMs)
+- **Goal**: Enable consistent tracing across multi-hop XCM flows
+
+Follow these steps to complete the multi-hop manual `SetTopic` flow:
+
+1. Create a new file named `initiate-reserve-withdraw-with-set-topic.ts` and add the following code:
+
+ ```ts
+ --8<-- 'code/tutorials/interoperability/xcm-observability-in-action/initiate-reserve-withdraw-with-set-topic.ts'
+ ```
+
+2. Run your script with the following command:
+
+ ```bash
+ npx tsx initiate-reserve-withdraw-with-set-topic.ts
+ ```
+
+3. You will see terminal output similar to the following. Note, the same `message_id` is present in all relevant events across chains:
+
+ --8<-- 'code/tutorials/interoperability/xcm-observability-in-action/initiate-reserve-withdraw-with-set-topic-result.html'
+
+### Forwarded XCM Example (Hydration)
+
+The following example illustrates how runtime preserves your `SetTopic` throughout the multi-hop flow:
+
+--8<-- 'code/tutorials/interoperability/xcm-observability-in-action/forwarded-xcm-remote-topic.html'
+
+## Script Troubleshooting Tips
+
+### Error: "Processed Message ID is `undefined`"
+
+This error usually means that the message has not yet been processed within the default retry window. If you see the following error when running a script:
+
+> ❌ Processed Message ID on Hydration is undefined. Try increasing MAX_RETRIES to wait for block finalisation.
+
+Update your script to increase the `MAX_RETRIES` value to give the chain more time:
+
+```ts
+const MAX_RETRIES = 8; // Number of attempts to wait for block finalisation
+```
+
+### Error: `PolkadotXcm.Sent` Event Not Found
+
+If you encounter an error indicating that `PolkadotXcm.Sent` is unavailable, like the following:
+
+> ⚠️ PolkadotXcm.Sent is only available in runtimes built from stable2503-5 or later.
+
+This error usually means that your runtime needs to be updated. Ensure that `wasm-override` is updated to runtime version 1.6.0+, or to any runtime built from `stable2503-5` or later.
+
+For details on updating your workspace, see [Setting Up Your Workspace](#setting-up-your-workspace).
+
+## Conclusion
+
+Congratulations! By completing this guide, you should now understand:
+
+- How `SetTopic` and `message_id` enable tracing and correlating XCMs across chains.
+- How to interpret and debug XCM failure cases.
+- How to manually and automatically manage topics for multi-hop flows.
+- How to use the legacy workaround for older runtimes with derived IDs.
+
+With these scenarios and debugging steps, you can confidently develop, trace, and troubleshoot XCM workflows across chains.
+
+## Additional Resources
+
+To learn more about XCM Observability features and best practices, see [XCM Observability](/develop/interoperability/xcm-observability){target=\_blank}.
\ No newline at end of file