Skip to content

Commit 9ed7bbc

Browse files
Create XCM Observability Guide (#873)
Pulling in external contributions for formatting review. * Copy XCM Observability * Apply the new format * Update Induction * Update Prerequisites * Update Where to Go Next * Add Understanding the Basics * Add scripts * Add result * Update Failure Event Handling * Add forwardIdFor * Update Workaround for Older Runtimes * Update Where to Go Next * Update Prerequisites * Remove Where to Go Next * Reorder the content * Change to Hydration * Update script * Update script * Check if PolkadotXcm.Sent exists * Add Define a Scenario: Multi-Hop XCM with Manual `SetTopic` * Add multi-hop-with-set-topic.ts * Add results * Update the order * Check grammar * Update Prerequisites * Update scripts * Add Set Up Your Project * Update script name * Update header * Update limited-reserve-transfer-assets.ts * Update scripts * Update deposit-reserve-asset-with-set-topic.ts * Update initiate-reserve-withdraw-with-set-topic.ts * Add forwarded-xcm-remote-topic.html * Update XCM Transfer with Manual `SetTopic` * Fix typos * Add Multi-hop XCM Transfer with Manual `SetTopic` * Update fully * Update partially * Update flowchart * Update flowchart * Update flowchart * Break into two lines * Break into two lines * Use TD * Update flowchart * Update limited_reserve_transfer_assets * Update DepositReserveAsset * Update InitiateReserveWithdraw * Update scripts * Update MAX_RETRIES * Fix fmt * Fix fmt * Update Prerequisites * Update to Polkadot Hub * Update to Polkadot Hub * Add Observability Features * Update wordings * Update Observability Features * Update Automatic vs Manual `SetTopic` * Update Summary * Fix output files * Change the exchange rate * Fix conflicts * Fix conflicts * Revert changes * Revert changes * Revert changes * Revert changes * Revert changes * Revert changes * Revert changes * Change Setting Up Your Workspace * Add links to Rust docs * Update limited-reserve-transfer-assets.ts * Update deposit-reserve-asset-with-set-topic.ts * Update initiate-reserve-withdraw-with-set-topic.ts * Fix imports * Fix typos * Add Troubleshooting on Running Scripts * Revert unrelated changes * Revert unrelated changes * Use 1.6.0 or later * Fix merge conflicts * Update tutorials/interoperability/xcm-observability.md Co-authored-by: Nicolás Hussein <[email protected]> * Add XCM Observability in Action * Update XCM Observability in Action * Update XCM Observability * Fix execution-with-error.html * Move Workaround for Older Runtimes * Use blake2b instead of blake2AsU8a * Use blake2b instead of blake2AsU8a * Add WithUniqueTopic * Add on system chains * Add on system chains --------- Co-authored-by: Nicolás Hussein <[email protected]>
1 parent f1fb6f9 commit 9ed7bbc

16 files changed

+2575
-1
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<div class="termynal" data-termynal>
2+
<pre data-ty>
3+
"error": {
4+
"type": "Module",
5+
"value": {
6+
"type": "PolkadotXcm",
7+
"value": {
8+
"type": "LocalExecutionIncompleteWithError",
9+
"value": {
10+
"index": 0,
11+
"error": {
12+
"type": "FailedToTransactAsset"
13+
}
14+
}
15+
}
16+
}
17+
}
18+
</pre>
19+
</div>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {blake2b} from "@noble/hashes/blake2";
2+
import {fromHex, mergeUint8, toHex} from "@polkadot-api/utils";
3+
import {Binary} from "polkadot-api";
4+
5+
function forwardIdFor(originalMessageId: string): string {
6+
// Decode the hex original_id into bytes
7+
const messageIdBytes = fromHex(originalMessageId);
8+
9+
// Create prefixed input: b"forward_id_for" + original_id
10+
const prefix = Binary.fromText("forward_id_for").asBytes();
11+
const input = mergeUint8([prefix, messageIdBytes]);
12+
13+
// Hash it using blake2b with 32-byte output
14+
const forwardedIdBytes = blake2b(input, {dkLen: 32});
15+
// Convert to hex
16+
return toHex(forwardedIdBytes);
17+
}
18+
19+
// Example: Forwarded ID from an original_id
20+
const originalMessageId = "0x5c082b4750ee8c34986eb22ce6e345bad2360f3682cda3e99de94b0d9970cb3e";
21+
22+
// Create the forwarded ID
23+
const forwardedIdHex = forwardIdFor(originalMessageId);
24+
25+
console.log("🔄 Forwarded ID:", forwardedIdHex);
26+
27+
const expectedForwardedId = "0xb3ae32fd2e2f798e8215865a8950d19df8330843608d4ee44e9f86849029724a";
28+
if (forwardedIdHex === expectedForwardedId) {
29+
console.log("✅ Forwarded ID matches expected value.");
30+
} else {
31+
console.error("❌ Forwarded ID does not match expected value.");
32+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div class="termynal" data-termynal>
2+
<span data-ty="input">npx tsx limited-reserve-transfer-assets.ts</span>
3+
<span data-ty>✅ Local dry run successful.</span>
4+
<span data-ty>📦 Finalised on Polkadot Hub in block #9471830: 0x98bd858739b3b5dd558def60cbd85d5e7fb2f4e33b0c00e1895e316541d727d9</span>
5+
<span data-ty>📣 Last message sent on Polkadot Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2</span>
6+
<span data-ty>✅ Sent Message ID on Polkadot Hub matched.</span>
7+
<span data-ty>📦 Finalised on Hydration in block #8749233: 0xe1413c5126698d7189d6f55a38e62d07ea4915078c2b1f3914d70f670e79e162</span>
8+
<span data-ty>📣 Last message processed on Hydration: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2</span>
9+
<span data-ty>✅ Processed Message ID on Hydration matched.</span>
10+
</div>
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import {Binary, createClient, Enum, type BlockInfo, type PolkadotClient} from "polkadot-api";
2+
import {withPolkadotSdkCompat} from "polkadot-api/polkadot-sdk-compat";
3+
import {getPolkadotSigner} from "polkadot-api/signer";
4+
import {getWsProvider} from "polkadot-api/ws-provider/web";
5+
import {
6+
assetHub,
7+
hydration,
8+
XcmV3MultiassetFungibility,
9+
XcmV3WeightLimit,
10+
XcmV5AssetFilter,
11+
XcmV5Instruction,
12+
XcmV5Junction,
13+
XcmV5Junctions,
14+
XcmV5WildAsset,
15+
XcmVersionedXcm,
16+
} from "@polkadot-api/descriptors";
17+
import {sr25519CreateDerive} from "@polkadot-labs/hdkd";
18+
import {
19+
DEV_PHRASE,
20+
entropyToMiniSecret,
21+
mnemonicToEntropy,
22+
ss58Address,
23+
} from "@polkadot-labs/hdkd-helpers";
24+
25+
const XCM_VERSION = 5;
26+
const MAX_RETRIES = 8; // Number of attempts to wait for block finalisation
27+
28+
const toHuman = (_key: any, value: any) => {
29+
if (typeof value === "bigint") {
30+
return Number(value);
31+
}
32+
33+
if (value && typeof value === "object" && typeof value.asHex === "function") {
34+
return value.asHex();
35+
}
36+
37+
return value;
38+
};
39+
40+
async function assertProcessedMessageId(
41+
client: PolkadotClient,
42+
api: any,
43+
name: String,
44+
blockBefore: BlockInfo,
45+
expectedMessageId: String,
46+
) {
47+
let processedMessageId = undefined;
48+
for (let i = 0; i < MAX_RETRIES; i++) {
49+
const blockAfter = await client.getFinalizedBlock();
50+
if (blockAfter.number == blockBefore.number) {
51+
const waiting = 1_000 * (2 ** i);
52+
console.log(`⏳ Waiting ${waiting / 1_000}s for ${name} block to be finalised (${i + 1}/${MAX_RETRIES})...`);
53+
await new Promise((resolve) => setTimeout(resolve, waiting));
54+
continue;
55+
}
56+
57+
console.log(`📦 Finalised on ${name} in block #${blockAfter.number}: ${blockAfter.hash}`);
58+
const processedEvents = await api.event.MessageQueue.Processed.pull();
59+
const processingFailedEvents = await api.event.MessageQueue.ProcessingFailed.pull();
60+
if (processedEvents.length > 0) {
61+
processedMessageId = processedEvents[0].payload.id.asHex();
62+
console.log(`📣 Last message processed on ${name}: ${processedMessageId}`);
63+
break;
64+
} else if (processingFailedEvents.length > 0) {
65+
processedMessageId = processingFailedEvents[0].payload.id.asHex();
66+
console.log(`📣 Last message ProcessingFailed on ${name}: ${processedMessageId}`);
67+
break;
68+
} else {
69+
console.log(`📣 No Processed events on ${name} found.`);
70+
blockBefore = blockAfter; // Update the block before to the latest one
71+
}
72+
}
73+
74+
if (processedMessageId === expectedMessageId) {
75+
console.log(`✅ Processed Message ID on ${name} matched.`);
76+
} else if (processedMessageId === undefined) {
77+
console.error(`❌ Processed Message ID on ${name} is undefined. Try increasing MAX_RETRIES to wait for block finalisation.`);
78+
} else {
79+
console.error(`❌ Processed Message ID [${processedMessageId}] on ${name} doesn't match expected Message ID [${expectedMessageId}].`);
80+
}
81+
}
82+
83+
async function main() {
84+
const para1Name = "Polkadot Hub";
85+
const para1Client = createClient(
86+
withPolkadotSdkCompat(getWsProvider("ws://localhost:8000")),
87+
);
88+
const para1Api = para1Client.getTypedApi(assetHub);
89+
90+
const para2Name = "Hydration";
91+
const para2Client = createClient(
92+
withPolkadotSdkCompat(getWsProvider("ws://localhost:8001")),
93+
);
94+
const para2Api = para2Client.getTypedApi(hydration);
95+
96+
const entropy = mnemonicToEntropy(DEV_PHRASE);
97+
const miniSecret = entropyToMiniSecret(entropy);
98+
const derive = sr25519CreateDerive(miniSecret);
99+
const alice = derive("//Alice");
100+
const alicePublicKey = alice.publicKey;
101+
const aliceSigner = getPolkadotSigner(alicePublicKey, "Sr25519", alice.sign);
102+
const aliceAddress = ss58Address(alicePublicKey);
103+
104+
const origin = Enum("system", Enum("Signed", aliceAddress));
105+
const beneficiary = {
106+
parents: 0,
107+
interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
108+
id: Binary.fromHex("0x9818ff3c27d256631065ecabf0c50e02551e5c5342b8669486c1e566fcbf847f")
109+
})),
110+
}
111+
const expectedMessageId = "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2";
112+
113+
const message = XcmVersionedXcm.V5([
114+
XcmV5Instruction.WithdrawAsset([
115+
{
116+
id: {
117+
parents: 1,
118+
interior: XcmV5Junctions.Here(),
119+
},
120+
fun: XcmV3MultiassetFungibility.Fungible(1_000_000_000n),
121+
},
122+
]),
123+
124+
XcmV5Instruction.ClearOrigin(),
125+
126+
XcmV5Instruction.BuyExecution({
127+
fees: {
128+
id: {
129+
parents: 1,
130+
interior: XcmV5Junctions.Here(),
131+
},
132+
fun: XcmV3MultiassetFungibility.Fungible(500_000_000n),
133+
},
134+
weight_limit: XcmV3WeightLimit.Unlimited(),
135+
}),
136+
137+
XcmV5Instruction.DepositReserveAsset({
138+
assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.All()),
139+
dest: {
140+
parents: 1,
141+
interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(2034)),
142+
},
143+
xcm: [
144+
XcmV5Instruction.BuyExecution({
145+
fees: {
146+
id: {
147+
parents: 1,
148+
interior: XcmV5Junctions.Here(),
149+
},
150+
fun: XcmV3MultiassetFungibility.Fungible(500_000_000n),
151+
},
152+
weight_limit: XcmV3WeightLimit.Unlimited(),
153+
}),
154+
155+
XcmV5Instruction.DepositAsset({
156+
assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.All()),
157+
beneficiary,
158+
}),
159+
],
160+
}),
161+
162+
XcmV5Instruction.SetTopic(Binary.fromHex(expectedMessageId)),
163+
]);
164+
165+
const weight: any = await para1Api.apis.XcmPaymentApi.query_xcm_weight(message);
166+
if (weight.success !== true) {
167+
console.error("❌ Failed to query XCM weight:", weight.error);
168+
para1Client.destroy();
169+
return;
170+
}
171+
172+
const tx: any = para1Api.tx.PolkadotXcm.execute({
173+
message,
174+
max_weight: weight.value,
175+
});
176+
const decodedCall: any = tx.decodedCall;
177+
console.log("👀 Executing XCM:", JSON.stringify(decodedCall, toHuman, 2));
178+
179+
try {
180+
const dryRunResult: any = await para1Api.apis.DryRunApi.dry_run_call(
181+
origin,
182+
decodedCall,
183+
XCM_VERSION,
184+
);
185+
console.log("📦 Dry run result:", JSON.stringify(dryRunResult.value, toHuman, 2));
186+
187+
const executionResult = dryRunResult.value.execution_result;
188+
if (!dryRunResult.success || !executionResult.success) {
189+
console.error("❌ Local dry run failed!");
190+
} else {
191+
console.log("✅ Local dry run successful.");
192+
193+
const emittedEvents: [any] = dryRunResult.value.emitted_events;
194+
const polkadotXcmSentEvent = emittedEvents.find(event =>
195+
event.type === "PolkadotXcm" && event.value.type === "Sent"
196+
);
197+
if (polkadotXcmSentEvent === undefined) {
198+
console.log(`⚠️ PolkadotXcm.Sent is only available in runtimes built from stable2503-5 or later.`);
199+
} else {
200+
let para2BlockBefore = await para2Client.getFinalizedBlock();
201+
const extrinsic = await tx.signAndSubmit(aliceSigner);
202+
const para1Block = extrinsic.block;
203+
console.log(`📦 Finalised on ${para1Name} in block #${para1Block.number}: ${para1Block.hash}`);
204+
205+
if (!extrinsic.ok) {
206+
const dispatchError = extrinsic.dispatchError;
207+
if (dispatchError.type === "Module") {
208+
const modErr: any = dispatchError.value;
209+
console.error(`❌ Dispatch error in module: ${modErr.type}${modErr.value?.type}`);
210+
} else {
211+
console.error("❌ Dispatch error:", JSON.stringify(dispatchError, toHuman, 2));
212+
}
213+
}
214+
215+
const sentEvents: any = await para1Api.event.PolkadotXcm.Sent.pull();
216+
if (sentEvents.length > 0) {
217+
const sentMessageId = sentEvents[0].payload.message_id.asHex();
218+
console.log(`📣 Last message sent on ${para1Name}: ${sentMessageId}`);
219+
if (sentMessageId === expectedMessageId) {
220+
console.log(`✅ Sent Message ID on ${para1Name} matched.`);
221+
} else {
222+
console.error(`❌ Sent Message ID [${sentMessageId}] on ${para1Name} doesn't match expected Message ID [${expectedMessageId}].`);
223+
}
224+
await assertProcessedMessageId(para2Client, para2Api, para2Name, para2BlockBefore, expectedMessageId);
225+
} else {
226+
console.log(`📣 No Sent events on ${para1Name} found.`);
227+
}
228+
}
229+
}
230+
} finally {
231+
para1Client.destroy();
232+
para2Client.destroy();
233+
}
234+
}
235+
236+
main().catch(console.error);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<div class="termynal" data-termynal>
2+
<pre data-ty>
3+
[
4+
{
5+
"type": "ReserveAssetDeposited",
6+
"value": [...]
7+
},
8+
{
9+
"type": "ClearOrigin"
10+
},
11+
{
12+
"type": "BuyExecution",
13+
"value": {...}
14+
},
15+
{
16+
"type": "DepositAsset",
17+
"value": {...}
18+
},
19+
{
20+
"type": "SetTopic",
21+
"value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2"
22+
}
23+
]
24+
</pre>
25+
</div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<div class="termynal" data-termynal>
2+
<pre data-ty>
3+
[
4+
{
5+
"type": "ReserveAssetDeposited",
6+
"value": [...]
7+
},
8+
{
9+
"type": "ClearOrigin"
10+
},
11+
{
12+
"type": "BuyExecution",
13+
"value": {...}
14+
},
15+
{
16+
"type": "ExchangeAsset",
17+
"value": {...}
18+
},
19+
{
20+
"type": "InitiateReserveWithdraw",
21+
"value": {
22+
"assets": {...},
23+
"reserve": {...},
24+
"xcm": [
25+
{
26+
"type": "BuyExecution",
27+
"value": {...}
28+
},
29+
{
30+
"type": "DepositAsset",
31+
"value": {...}
32+
},
33+
{
34+
"type": "SetTopic",
35+
"value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2"
36+
}
37+
]
38+
}
39+
},
40+
{
41+
"type": "SetTopic",
42+
"value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2"
43+
}
44+
]
45+
</pre>
46+
</div>

0 commit comments

Comments
 (0)