Skip to content

Commit 9301805

Browse files
Add Multi-hop XCM Transfer with Manual SetTopic
1 parent 508cc4d commit 9301805

File tree

2 files changed

+380
-8
lines changed

2 files changed

+380
-8
lines changed

llms.txt

Lines changed: 341 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27837,7 +27837,7 @@ You will learn how to:
2783727837
- Use a workaround for older runtimes that emit derived message identifiers
2783827838
- Interpret and handle failed or incomplete messages
2783927839

27840-
To demonstrate these techniques, the guide introduces a complete example scenario involving a multi-chain XCM transfer. This scenario will serve as the foundation for explaining the message lifecycle, event tracking, and failure debugging in context.
27840+
To demonstrate these techniques, the guide introduces a complete example scenario involving a multichain XCM transfer. This scenario will serve as the foundation for explaining the message lifecycle, event tracking, and failure debugging in context.
2784127841

2784227842
## Prerequisites
2784327843

@@ -27954,7 +27954,7 @@ This example uses the `PolkadotXcm.limited_reserve_transfer_assets` extrinsic to
2795427954

2795527955
The runtime automatically appends a `SetTopic` instruction to the forwarded XCM. This topic becomes the `message_id` used in both `Sent` and `Processed` events, enabling traceability without manual intervention.
2795627956

27957-
Create a new script, `limited-reserve-transfer-assets.ts`
27957+
Create a new script named `limited-reserve-transfer-assets.ts`
2795827958

2795927959
```ts
2796027960
import {Binary, createClient, Enum} from "polkadot-api";
@@ -28199,14 +28199,14 @@ After submitting the transfer, use the `message_id` to correlate origin and dest
2819928199

2820028200
### Define a Scenario: XCM Transfer with Manual `SetTopic`
2820128201

28202-
In multi-chain XCM flows, such as transferring assets between two chains, you may want to include a `SetTopic` instruction to **reliably trace the message across all involved chains**.
28202+
In multichain XCM flows, such as transferring assets between two chains, you may want to include a `SetTopic` instruction to **reliably trace the message across all involved chains**.
2820328203

2820428204
- **Origin chain**: Polkadot Asset Hub
2820528205
- **Destination chain**: Hydration
2820628206
- **Topic**: Manually assigned via `SetTopic` instruction
2820728207
- **Goal**: Transfer DOT and trace the XCM using the assigned `message_id`
2820828208

28209-
Create a new script, `deposit-reserve-asset-with-set-topic.ts`
28209+
Create a new script named `deposit-reserve-asset-with-set-topic.ts`
2821028210

2821128211
```ts
2821228212
import {Binary, BlockInfo, createClient, Enum, PolkadotClient, TypedApi} from "polkadot-api";
@@ -28469,7 +28469,344 @@ code/tutorials/interoperability/xcm-observability/forwarded-xcm-custom.html
2846928469
<span data-ty>✅ Processed Message ID on Hydration matched.</span>
2847028470
</div>
2847128471

28472+
### Define a Scenario: Multi-hop XCM Transfer with Manual `SetTopic`
2847228473

28474+
In multichain XCM flows—such as sending assets from one chain to another and then back, you can include a `SetTopic` instruction to **consistently trace the message across all hops**.
28475+
28476+
- **Origin chain**: Polkadot Asset Hub
28477+
- **Destination chain**: Hydration
28478+
- **Topic**: Manually assigned via `SetTopic` instructions
28479+
- **Goal**: Transfer DOT and track the XCM using the assigned `message_id` across both chains
28480+
28481+
Create a new script named `initiate-reserve-withdraw-with-set-topic.ts`:
28482+
28483+
```ts
28484+
import {Binary, BlockInfo, createClient, Enum, PolkadotClient, TypedApi} from "polkadot-api";
28485+
import {withPolkadotSdkCompat} from "polkadot-api/polkadot-sdk-compat";
28486+
import {getPolkadotSigner} from "polkadot-api/signer";
28487+
import {getWsProvider} from "polkadot-api/ws-provider/web";
28488+
import {
28489+
assetHub,
28490+
hydration,
28491+
XcmV2MultiassetWildFungibility,
28492+
XcmV3MultiassetFungibility,
28493+
XcmV3WeightLimit,
28494+
XcmV5AssetFilter,
28495+
XcmV5Instruction,
28496+
XcmV5Junction,
28497+
XcmV5Junctions,
28498+
XcmV5WildAsset,
28499+
XcmVersionedXcm,
28500+
} from "@polkadot-api/descriptors";
28501+
import {sr25519CreateDerive} from "@polkadot-labs/hdkd";
28502+
import {
28503+
DEV_PHRASE,
28504+
entropyToMiniSecret,
28505+
mnemonicToEntropy,
28506+
ss58Address,
28507+
} from "@polkadot-labs/hdkd-helpers";
28508+
28509+
const XCM_VERSION = 5;
28510+
28511+
const toHuman = (_key: any, value: any) => {
28512+
if (typeof value === "bigint") {
28513+
return Number(value);
28514+
}
28515+
28516+
if (value && typeof value === "object" && typeof value.asHex === "function") {
28517+
return value.asHex();
28518+
}
28519+
28520+
return value;
28521+
};
28522+
28523+
async function getProcessedMessageId(client: PolkadotClient, api: TypedApi<any>, name: String, blockBefore: BlockInfo): Promise<String> {
28524+
let processedMessageId = undefined;
28525+
const maxRetries = 8;
28526+
for (let i = 0; i < maxRetries; i++) {
28527+
const blockAfter = await client.getFinalizedBlock();
28528+
if (blockAfter.number == blockBefore.number) {
28529+
const waiting = 1_000 * (2 ** i);
28530+
console.log(`⏳ Waiting ${waiting / 1_000}s for ${name} block to be finalised (${i + 1}/${maxRetries})...`);
28531+
await new Promise((resolve) => setTimeout(resolve, waiting));
28532+
continue;
28533+
}
28534+
28535+
console.log(`📦 Finalised on ${name} in block #${blockAfter.number}: ${blockAfter.hash}`);
28536+
const processedEvents = await api.event.MessageQueue.Processed.pull();
28537+
const processingFailedEvents = await api.event.MessageQueue.ProcessingFailed.pull();
28538+
if (processedEvents.length > 0) {
28539+
processedMessageId = processedEvents[0].payload.id.asHex();
28540+
console.log(`📣 Last message processed on ${name}: ${processedMessageId}`);
28541+
break;
28542+
} else if (processingFailedEvents.length > 0) {
28543+
processedMessageId = processingFailedEvents[0].payload.id.asHex();
28544+
console.log(`📣 Last message ProcessingFailed on ${name}: ${processedMessageId}`);
28545+
break;
28546+
} else {
28547+
console.log(`📣 No Processed events on ${name} found.`);
28548+
blockBefore = blockAfter; // Update the block before to the latest one
28549+
}
28550+
}
28551+
28552+
return processedMessageId;
28553+
}
28554+
28555+
async function main() {
28556+
const para1Name = "Polkadot Asset Hub";
28557+
const para1Client = createClient(
28558+
withPolkadotSdkCompat(getWsProvider("ws://localhost:8000")),
28559+
);
28560+
const para1Api = para1Client.getTypedApi(assetHub);
28561+
28562+
const para2Name = "Hydration";
28563+
const para2Client = createClient(
28564+
withPolkadotSdkCompat(getWsProvider("ws://localhost:8001")),
28565+
);
28566+
const para2Api = para2Client.getTypedApi(hydration);
28567+
28568+
const entropy = mnemonicToEntropy(DEV_PHRASE);
28569+
const miniSecret = entropyToMiniSecret(entropy);
28570+
const derive = sr25519CreateDerive(miniSecret);
28571+
const alice = derive("//Alice");
28572+
const alicePublicKey = alice.publicKey;
28573+
const aliceSigner = getPolkadotSigner(alicePublicKey, "Sr25519", alice.sign);
28574+
const aliceAddress = ss58Address(alicePublicKey);
28575+
28576+
const origin = Enum("system", Enum("Signed", aliceAddress));
28577+
const beneficiary = {
28578+
parents: 0,
28579+
interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
28580+
id: Binary.fromHex("0x9818ff3c27d256631065ecabf0c50e02551e5c5342b8669486c1e566fcbf847f")
28581+
})),
28582+
}
28583+
const assetId = {
28584+
parents: 0,
28585+
interior: XcmV5Junctions.X2([
28586+
XcmV5Junction.PalletInstance(50),
28587+
XcmV5Junction.GeneralIndex(1984n),
28588+
]),
28589+
};
28590+
const giveId = {
28591+
parents: 1,
28592+
interior: XcmV5Junctions.X3([
28593+
XcmV5Junction.Parachain(1000),
28594+
XcmV5Junction.PalletInstance(50),
28595+
XcmV5Junction.GeneralIndex(1984n),
28596+
]),
28597+
};
28598+
const giveFun = XcmV3MultiassetFungibility.Fungible(1_500_000n);
28599+
const dest = {
28600+
parents: 1,
28601+
interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(2034)),
28602+
};
28603+
const wantId = {
28604+
parents: 1,
28605+
interior: XcmV5Junctions.Here(),
28606+
};
28607+
const wantFun = XcmV3MultiassetFungibility.Fungible(3_552_961_212n);
28608+
const expectedMessageId = "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2";
28609+
28610+
const message = XcmVersionedXcm.V5([
28611+
XcmV5Instruction.WithdrawAsset([{
28612+
id: assetId,
28613+
fun: giveFun,
28614+
}]),
28615+
28616+
XcmV5Instruction.SetFeesMode({jit_withdraw: true}),
28617+
28618+
XcmV5Instruction.DepositReserveAsset({
28619+
assets: XcmV5AssetFilter.Wild(
28620+
XcmV5WildAsset.AllOf({
28621+
id: assetId,
28622+
fun: XcmV2MultiassetWildFungibility.Fungible(),
28623+
})),
28624+
dest,
28625+
xcm: [
28626+
XcmV5Instruction.BuyExecution({
28627+
fees: {
28628+
id: giveId,
28629+
fun: giveFun,
28630+
},
28631+
weight_limit: XcmV3WeightLimit.Unlimited(),
28632+
}),
28633+
28634+
XcmV5Instruction.ExchangeAsset({
28635+
give: XcmV5AssetFilter.Wild(
28636+
XcmV5WildAsset.AllOf({
28637+
id: giveId,
28638+
fun: XcmV2MultiassetWildFungibility.Fungible(),
28639+
}),
28640+
),
28641+
want: [{
28642+
id: wantId,
28643+
fun: wantFun,
28644+
}],
28645+
maximal: false,
28646+
}),
28647+
28648+
XcmV5Instruction.InitiateReserveWithdraw({
28649+
assets: XcmV5AssetFilter.Wild(
28650+
XcmV5WildAsset.AllOf({
28651+
id: wantId,
28652+
fun: XcmV2MultiassetWildFungibility.Fungible(),
28653+
}),
28654+
),
28655+
reserve: {
28656+
parents: 1,
28657+
interior: XcmV5Junctions.X1(
28658+
XcmV5Junction.Parachain(1000),
28659+
),
28660+
},
28661+
xcm: [
28662+
XcmV5Instruction.BuyExecution({
28663+
fees: {
28664+
id: wantId,
28665+
fun: wantFun,
28666+
},
28667+
weight_limit: XcmV3WeightLimit.Unlimited(),
28668+
}),
28669+
28670+
XcmV5Instruction.DepositAsset({
28671+
assets: XcmV5AssetFilter.Wild(
28672+
XcmV5WildAsset.AllOf({
28673+
id: wantId,
28674+
fun: XcmV2MultiassetWildFungibility.Fungible(),
28675+
}),
28676+
),
28677+
beneficiary,
28678+
}),
28679+
28680+
XcmV5Instruction.SetTopic(Binary.fromHex(expectedMessageId)),
28681+
],
28682+
}),
28683+
],
28684+
}),
28685+
28686+
XcmV5Instruction.SetTopic(Binary.fromHex(expectedMessageId)),
28687+
]);
28688+
28689+
const weight: any =
28690+
await para1Api.apis.XcmPaymentApi.query_xcm_weight(message);
28691+
if (weight.success !== true) {
28692+
console.error("❌ Failed to query XCM weight:", weight.error);
28693+
para1Client.destroy();
28694+
return;
28695+
}
28696+
28697+
const tx: any = para1Api.tx.PolkadotXcm.execute({
28698+
message,
28699+
max_weight: weight.value,
28700+
});
28701+
const decodedCall = tx.decodedCall as any;
28702+
console.log("👀 Executing XCM:", JSON.stringify(decodedCall, toHuman, 2));
28703+
28704+
try {
28705+
const dryRunResult: any = await para1Api.apis.DryRunApi.dry_run_call(
28706+
origin,
28707+
decodedCall,
28708+
XCM_VERSION,
28709+
);
28710+
console.log("📦 Dry run result:", JSON.stringify(dryRunResult.value, toHuman, 2));
28711+
28712+
const executionResult = dryRunResult.value.execution_result;
28713+
if (!dryRunResult.success || !executionResult.success) {
28714+
console.error("❌ Local dry run failed!");
28715+
} else {
28716+
console.log("✅ Local dry run successful.");
28717+
28718+
const emittedEvents: [any] = dryRunResult.value.emitted_events;
28719+
const polkadotXcmSentEvent = emittedEvents.find(event =>
28720+
event.type === "PolkadotXcm" && event.value.type === "Sent"
28721+
);
28722+
if (polkadotXcmSentEvent === undefined) {
28723+
console.log(`⚠️ PolkadotXcm.Sent is available in runtimes built from stable2503-5 or later.`);
28724+
} else {
28725+
let para2BlockBefore = await para2Client.getFinalizedBlock();
28726+
const extrinsic = await tx.signAndSubmit(aliceSigner);
28727+
const para1BlockBefore = extrinsic.block;
28728+
console.log(`📦 Finalised on ${para1Name} in block #${para1BlockBefore.number}: ${para1BlockBefore.hash}`);
28729+
28730+
if (!extrinsic.ok) {
28731+
const dispatchError = extrinsic.dispatchError;
28732+
if (dispatchError.type === "Module") {
28733+
const modErr: any = dispatchError.value;
28734+
console.error(`❌ Dispatch error in module: ${modErr.type} → ${modErr.value?.type}`);
28735+
} else {
28736+
console.error("❌ Dispatch error:", JSON.stringify(dispatchError, toHuman, 2));
28737+
}
28738+
}
28739+
28740+
const sentEvents = await para1Api.event.PolkadotXcm.Sent.pull();
28741+
if (sentEvents.length > 0) {
28742+
const sentMessageId = sentEvents[0].payload.message_id.asHex();
28743+
console.log(`📣 Last message sent on ${para1Name}: ${sentMessageId}`);
28744+
if (sentMessageId === expectedMessageId) {
28745+
console.log(`✅ Sent Message ID on ${para1Name} matched.`);
28746+
} else {
28747+
console.error(`❌ Sent Message ID [${sentMessageId}] on ${para1Name} doesn't match expexted Message ID [${expectedMessageId}].`);
28748+
}
28749+
28750+
let processedMessageId = await getProcessedMessageId(para2Client, para2Api, para2Name, para2BlockBefore);
28751+
if (processedMessageId === expectedMessageId) {
28752+
console.log(`✅ Processed Message ID on ${para2Name} matched.`);
28753+
} else {
28754+
console.error(`❌ Processed Message ID [${processedMessageId}] on ${para2Name} doesn't match expected Message ID [${expectedMessageId}].`);
28755+
}
28756+
28757+
let processedMessageIdOnPara1 = await getProcessedMessageId(para1Client, para1Api, para1Name, para1BlockBefore);
28758+
if (processedMessageIdOnPara1 === expectedMessageId) {
28759+
console.log(`✅ Processed Message ID on ${para1Name} matched.`);
28760+
} else {
28761+
console.error(`❌ Processed Message ID [${processedMessageIdOnPara1}] on ${para1Name} doesn't match expected Message ID [${expectedMessageId}].`);
28762+
}
28763+
} else {
28764+
console.log(`📣 No Sent events on ${para1Name} found.`);
28765+
}
28766+
}
28767+
}
28768+
} finally {
28769+
para1Client.destroy();
28770+
para2Client.destroy();
28771+
}
28772+
}
28773+
28774+
main().catch(console.error);
28775+
```
28776+
28777+
Run it locally:
28778+
28779+
```bash
28780+
npx tsx initiate-reserve-withdraw-with-set-topic.ts
28781+
```
28782+
28783+
#### Forwarded XCM (Destination Chain: Hydration)
28784+
28785+
During execution, the runtime applies your `SetTopic` instruction, ensuring the same topic is preserved throughout the cross-chain flow:
28786+
28787+
```html
28788+
code/tutorials/interoperability/xcm-observability/forwarded-xcm-remote.html
28789+
```
28790+
28791+
#### Example: Message Trace Output
28792+
28793+
Below is the actual end-to-end trace, showing the same `message_id` at each step across all involved chains:
28794+
28795+
```html
28796+
<div class="termynal" data-termynal>
28797+
<span data-ty="input">npx tsx initiate-reserve-withdraw-with-set-topic.ts</span>
28798+
<span data-ty>✅ Local dry run successful.</span>
28799+
<span data-ty>📦 Finalised on Polkadot Asset Hub in block #9471831: 0x2620f7e29765fc953263b7835711011616702c9d82ef5306fe3ef4196cb75cab</span>
28800+
<span data-ty>📣 Last message sent on Polkadot Asset Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2</span>
28801+
<span data-ty>✅ Sent Message ID on Polkadot Asset Hub matched.</span>
28802+
<span data-ty>📦 Finalised on Hydration in block #8749235: 0xafe7f6149b1773a8d3d229040cda414aafd64baaeffa37fb4a5b2a542308b2d6</span>
28803+
<span data-ty>📣 Last message processed on Hydration: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2</span>
28804+
<span data-ty>✅ Processed Message ID on Hydration matched.</span>
28805+
<span data-ty>📦 Finalised on Polkadot Asset Hub in block #9471832: 0x7c150b69e3562694f0573e4fee73dfb86f3ab71b808679a1777586ff24643e9a</span>
28806+
<span data-ty>📣 Last message processed on Polkadot Asset Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2</span>
28807+
<span data-ty>✅ Processed Message ID on Polkadot Asset Hub matched.</span>
28808+
</div>
28809+
```
2847328810

2847428811
## Workaround for Older Runtimes
2847528812

0 commit comments

Comments
 (0)