Skip to content

Instantly share code, notes, and snippets.

@franciscoaguirre
Last active September 9, 2025 00:19
Show Gist options
  • Save franciscoaguirre/a6dea0c55e81faba65bedf700033a1a2 to your computer and use it in GitHub Desktop.
Save franciscoaguirre/a6dea0c55e81faba65bedf700033a1a2 to your computer and use it in GitHub Desktop.
Interacting with the XCM precompile with Hardhat
import {
passetHub,
XcmV3MultiassetFungibility,
XcmV3WeightLimit,
XcmV5AssetFilter,
XcmV5Instruction,
XcmV5Junction,
XcmV5Junctions,
XcmV5WildAsset,
XcmVersionedXcm,
} from "@polkadot-api/descriptors";
import { Binary, FixedSizeBinary, getTypedCodecs } from "polkadot-api";
const PAS_UNITS = 10_000_000_000n;
const PAS_CENTS = 100_000_000n;
const ACCOUNT = "<polkadot-ss58-account>";
export async function getSimpleTransfer(): Promise<string> {
const xcm = XcmVersionedXcm.V5([
XcmV5Instruction.WithdrawAsset([
{
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(1n * PAS_UNITS),
},
]),
XcmV5Instruction.PayFees({
asset: {
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(10n * PAS_CENTS),
},
}),
XcmV5Instruction.DepositAsset({
assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.AllCounted(1)),
beneficiary: {
parents: 0,
interior: XcmV5Junctions.X1(
XcmV5Junction.AccountId32({
network: undefined,
id: FixedSizeBinary.fromAccountId32(ACCOUNT),
})
),
},
}),
]);
const codecs = await getTypedCodecs(passetHub);
const xcmEncoded = codecs.apis.XcmPaymentApi.query_xcm_weight.args.enc([xcm]);
const xcmHex = Binary.fromBytes(xcmEncoded).asHex();
return xcmHex;
}
export async function getTeleport(paraId: number): Promise<string> {
const xcm = XcmVersionedXcm.V5([
XcmV5Instruction.WithdrawAsset([
{
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(1n * PAS_UNITS),
},
]),
XcmV5Instruction.PayFees({
asset: {
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(10n * PAS_CENTS),
},
}),
XcmV5Instruction.InitiateTransfer({
destination: {
parents: 1,
interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(paraId)),
},
remote_fees: Enum(
"Teleport",
XcmV5AssetFilter.Definite([
{
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(10n * PAS_CENTS),
},
])
),
preserve_origin: false,
remote_xcm: [
XcmV5Instruction.DepositAsset({
assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.AllCounted(1)),
beneficiary: {
parents: 0,
interior: XcmV5Junctions.X1(
XcmV5Junction.AccountId32({
network: undefined,
id: FixedSizeBinary.fromAccountId32(ACCOUNT),
})
),
},
}),
],
assets: [
Enum("Teleport", XcmV5AssetFilter.Wild(XcmV5WildAsset.AllCounted(1))), // We send everything.
],
}),
]);
const codecs = await getTypedCodecs(passetHub);
const xcmEncoded = codecs.apis.XcmPaymentApi.query_xcm_weight.args.enc([xcm]);
const xcmHex = Binary.fromBytes(xcmEncoded).asHex();
return xcmHex;
}
async function main() {
const hex = await getTeleport(1000);
console.log("Message:", hex);
}
const ASSET_HUB_PARA_ID = 1000;
// Sends PAS to a non-system parachain.
//
// They are routed via AssetHub.
export async function getTransferToPara(paraId: number) {
const xcm = XcmVersionedXcm.V5([
XcmV5Instruction.WithdrawAsset([
{
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(10n * PAS_UNITS),
},
]),
XcmV5Instruction.PayFees({
asset: {
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(1n * PAS_UNITS),
},
}),
XcmV5Instruction.InitiateTransfer({
destination: {
parents: 1,
interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(ASSET_HUB_PARA_ID)),
},
remote_fees: Enum(
"Teleport",
XcmV5AssetFilter.Definite([
{
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(1n * PAS_UNITS),
},
])
),
preserve_origin: false,
remote_xcm: [
XcmV5Instruction.DepositReserveAsset({
assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.AllCounted(1)),
dest: { parents: 1, interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(paraId)) },
xcm: [
XcmV5Instruction.BuyExecution({
fees: {
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(10n * PAS_CENTS),
},
weight_limit: XcmV3WeightLimit.Unlimited(),
}),
XcmV5Instruction.DepositAsset({
assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.AllCounted(1)),
beneficiary: {
parents: 0,
interior: XcmV5Junctions.X1(
XcmV5Junction.AccountId32({
network: undefined,
id: FixedSizeBinary.fromAccountId32(ACCOUNT),
})
),
},
}),
],
}),
],
assets: [
Enum("Teleport", XcmV5AssetFilter.Wild(XcmV5WildAsset.AllCounted(1))), // We send everything.
],
}),
]);
const codecs = await getTypedCodecs(passetHub);
const xcmEncoded = codecs.apis.XcmPaymentApi.query_xcm_weight.args.enc([xcm]);
const xcmHex = Binary.fromBytes(xcmEncoded).asHex();
return xcmHex;
}
main();
const hre = require("hardhat");
const { getSimpleTransfer, getTeleport, getTransferToPara } = require("./generate-xcm.js");
async function main() {
const precompileAddress = "0x00000000000000000000000000000000000A0000";
const xcm = await hre.ethers.getContractAt("IXcm", precompileAddress);
const message = await getTransferToPara(2034);
const weight = await xcm.weighMessage(message);
console.dir({ weight });
const tx = await xcm.execute(message, [weight[0], weight[1]]);
const receipt = await tx.wait();
console.dir({ receipt });
}
main()
.then(() => process.exit())
.catch((error) => {
console.error(error);
process.exit(1);
});
@CocDap
Copy link

CocDap commented Aug 14, 2025

hi @franciscoaguirre, Thanks for your example on how to use xcm-precompile. Although the transaction was successful, the XCM execution isn't working as expected

I'm getting an error: AssetsTrapped

Example: getSimpleTransfer

With my understanding, getSimpleTransfer is teleport native token from the Passet Hub to the Relay Chain

I have a few questions:

  1. Since we're using an EVM account to sign the execute transaction, which account—the EVM one or a Substrate one—should the WithdrawAsset instruction be based on?

  2. Are the EVM account and the Substrate account derived from the same private key? If not, what's the formula for mapping between the two?

image

Hopefully, you can response me soon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment