-
Notifications
You must be signed in to change notification settings - Fork 74
Create XCM Observability Guide (#873) #980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9ed7bbc
ffede2e
58fcc29
57bf86b
5551dd3
ef9329b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <div class="termynal" data-termynal> | ||
| <pre data-ty> | ||
| "error": { | ||
| "type": "Module", | ||
| "value": { | ||
| "type": "PolkadotXcm", | ||
| "value": { | ||
| "type": "LocalExecutionIncompleteWithError", | ||
| "value": { | ||
| "index": 0, | ||
| "error": { | ||
| "type": "FailedToTransactAsset" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| </pre> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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."); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <div class="termynal" data-termynal> | ||
| <span data-ty="input">npx tsx limited-reserve-transfer-assets.ts</span> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is missing the file path span |
||
| <span data-ty>✅ Local dry run successful.</span> | ||
| <span data-ty>📦 Finalised on Polkadot Hub in block #9471830: 0x98bd858739b3b5dd558def60cbd85d5e7fb2f4e33b0c00e1895e316541d727d9</span> | ||
| <span data-ty>📣 Last message sent on Polkadot Hub: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2</span> | ||
| <span data-ty>✅ Sent Message ID on Polkadot Hub matched.</span> | ||
| <span data-ty>📦 Finalised on Hydration in block #8749233: 0xe1413c5126698d7189d6f55a38e62d07ea4915078c2b1f3914d70f670e79e162</span> | ||
| <span data-ty>📣 Last message processed on Hydration: 0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2</span> | ||
| <span data-ty>✅ Processed Message ID on Hydration matched.</span> | ||
| </div> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please format |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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 | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| 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); | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| <div class="termynal" data-termynal> | ||
| <pre data-ty> | ||
| [ | ||
| { | ||
| "type": "ReserveAssetDeposited", | ||
| "value": [...] | ||
| }, | ||
| { | ||
| "type": "ClearOrigin" | ||
| }, | ||
| { | ||
| "type": "BuyExecution", | ||
| "value": {...} | ||
| }, | ||
| { | ||
| "type": "DepositAsset", | ||
| "value": {...} | ||
| }, | ||
| { | ||
| "type": "SetTopic", | ||
| "value": "0xd60225f721599cb7c6e23cdf4fab26f205e30cd7eb6b5ccf6637cdc80b2339b2" | ||
| } | ||
| ] | ||
| </pre> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| <div class="termynal" data-termynal> | ||
| <pre data-ty> | ||
| [ | ||
| { | ||
| "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" | ||
| } | ||
| ] | ||
| </pre> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file needs to be formatted, please