Skip to content

Instantly share code, notes, and snippets.

@franciscoaguirre
Last active June 17, 2025 12:57
Show Gist options
  • Select an option

  • Save franciscoaguirre/c1b2a9480744bbe698bfd74f9a0c0e26 to your computer and use it in GitHub Desktop.

Select an option

Save franciscoaguirre/c1b2a9480744bbe698bfd74f9a0c0e26 to your computer and use it in GitHub Desktop.
XCM Teleport with accurate fees
// `ahp` is the name we gave to `bun papi add`.
import {
ahp,
DispatchRawOrigin,
people,
XcmV3Junction,
XcmV3Junctions,
XcmV3MultiassetFungibility,
XcmV3WeightLimit,
XcmV4AssetAssetFilter,
XcmV4AssetWildAsset,
XcmV4Instruction,
XcmVersionedAssetId,
XcmVersionedLocation,
XcmVersionedXcm,
} from "@polkadot-api/descriptors";
import {
createClient,
Enum,
FixedSizeBinary,
} from "polkadot-api";
// import from "polkadot-api/ws-provider/node"
// if you are running in a NodeJS environment
import { getWsProvider } from "polkadot-api/ws-provider/web";
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
import { getAliceSigner, stringify } from "./utils";
// Useful constants.
// People.
const PEOPLE_PARA_ID = 1004;
// We're using localhost here since this was tested with chopsticks.
// For production, use a public rpc, for example: "wss://polkadot-people-rpc.polkadot.io".
const PEOPLE_WS_URL = "ws://localhost:8001";
// Asset Hub.
const ASSET_HUB_PARA_ID = 1000;
// We're using localhost here since this was tested with chopsticks.
// For production, use a public rpc, for example: "wss://polkadot-asset-hub-rpc.polkadot.io".
const ASSET_HUB_WS_URL = "ws://localhost:8000";
// How to get to People from the perspective of Asset Hub.
const PEOPLE_FROM_AH = {
parents: 1,
interior: XcmV3Junctions.X1(XcmV3Junction.Parachain(PEOPLE_PARA_ID)),
};
// XCM.
const XCM_VERSION = 4;
// DOT.
const DOT_UNITS = 10_000_000_000n;
const DOT_FROM_PARACHAINS = {
parents: 1,
interior: XcmV3Junctions.Here(),
};
// Alice's SS58 account for Polkadot.
const ACCOUNT = "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5";
// Setup client...
const ahClient = createClient(
withPolkadotSdkCompat(getWsProvider(ASSET_HUB_WS_URL)),
);
// ...and typed api.
const ahApi = ahClient.getTypedApi(ahp);
// The whole execution of the script.
await main();
// We'll teleport DOT from Asset Hub to People.
// Using the XcmPaymentApi and DryRunApi, we'll estimate the XCM fees accurately.
async function main() {
// The amount of DOT we wish to teleport.
const transferAmount = 10n * DOT_UNITS;
// We overestimate both local and remote fees, these will be adjusted by the dry run below.
const localFeesHighEstimate = 1n * DOT_UNITS;
const remoteFeesHighEstimate = 1n * DOT_UNITS;
// We create a tentative XCM, one with the high estimates for fees.
const tentativeXcm = createTeleport(
transferAmount,
localFeesHighEstimate,
remoteFeesHighEstimate,
);
console.dir(stringify(tentativeXcm));
// This will give us the adjusted estimates, much more accurate than before.
const [localFeesEstimate, remoteFeesEstimate] =
(await estimateFees(tentativeXcm))!;
// With these estimates, we can create the final XCM to execute.
const xcm = createTeleport(
transferAmount,
localFeesEstimate,
remoteFeesEstimate,
);
// We get the weight and we execute.
const weightResult = await ahApi.apis.XcmPaymentApi.query_xcm_weight(xcm);
if (weightResult.success) {
const tx = ahApi.tx.PolkadotXcm.execute({
message: xcm,
max_weight: weightResult.value,
});
const signer = getAliceSigner();
const result = await tx.signAndSubmit(signer);
console.dir(stringify(result));
}
}
// Creates an XCM that will teleport DOT from Asset Hub to People.
//
// Takes in the amount of DOT wanting to be transferred, as well as the
// amount of DOT willing to be used for local and remote fees.
function createTeleport(
transferAmount: bigint,
localFees: bigint,
remoteFees: bigint,
): XcmVersionedXcm {
const beneficiary = {
parents: 0,
interior: XcmV3Junctions.X1(
XcmV3Junction.AccountId32({
id: FixedSizeBinary.fromAccountId32(ACCOUNT),
}),
),
};
const dotToWithdraw = {
id: DOT_FROM_PARACHAINS,
fun: XcmV3MultiassetFungibility.Fungible(
transferAmount + localFees + remoteFees,
),
};
const dotForLocalFees = {
id: DOT_FROM_PARACHAINS,
fun: XcmV3MultiassetFungibility.Fungible(localFees),
};
const dotForRemoteFees = {
id: DOT_FROM_PARACHAINS,
fun: XcmV3MultiassetFungibility.Fungible(remoteFees),
};
const xcm = XcmVersionedXcm.V4([
XcmV4Instruction.WithdrawAsset([dotToWithdraw]),
XcmV4Instruction.BuyExecution({
fees: dotForLocalFees,
// We allow maximum weight bought with the specified fees.
weight_limit: XcmV3WeightLimit.Unlimited(),
}),
XcmV4Instruction.InitiateTeleport({
dest: PEOPLE_FROM_AH,
assets: XcmV4AssetAssetFilter.Wild(XcmV4AssetWildAsset.AllCounted(1)),
xcm: [
XcmV4Instruction.BuyExecution({
fees: dotForRemoteFees,
weight_limit: XcmV3WeightLimit.Unlimited(),
}),
XcmV4Instruction.DepositAsset({
assets: XcmV4AssetAssetFilter.Wild(XcmV4AssetWildAsset.AllCounted(1)),
beneficiary,
}),
],
}),
]);
return xcm;
}
// Estimates both local and remote fees for a given XCM.
//
// This is the mechanism showcased on this script.
// Uses the XcmPaymentApi to get local fees, both execution and delivery.
// Then uses the DryRunApi to get the sent XCM and estimates remote fees
// connecting to the destination chain.
//
// If there's any issue and fees couldn't be estimated, returns undefined.
async function estimateFees(
xcm: XcmVersionedXcm,
): Promise<[bigint, bigint] | undefined> {
const xcmWeight = await ahApi.apis.XcmPaymentApi.query_xcm_weight(xcm);
if (!xcmWeight.success) {
return;
}
// Execution fees are purely a function of the weight.
const executionFees =
await ahApi.apis.XcmPaymentApi.query_weight_to_asset_fee(
xcmWeight.value,
XcmVersionedAssetId.V4(DOT_FROM_PARACHAINS),
);
if (!executionFees.success) {
return;
}
const tx = ahApi.tx.PolkadotXcm.execute({
message: xcm,
max_weight: xcmWeight.value,
});
const dryRunResult = await ahApi.apis.DryRunApi.dry_run_call(
Enum("system", DispatchRawOrigin.Signed(ACCOUNT)),
tx.decodedCall,
XCM_VERSION,
);
if (!dryRunResult.success || !dryRunResult.value.execution_result.success) {
return;
}
// XCM execution might result in multiple messages being sent.
// That's why we need to search for our message in the `forwarded_xcms` array.
const [_, messages] = dryRunResult.value.forwarded_xcms.find(
([location, _]) =>
location.type === "V4" &&
location.value.parents === 1 &&
location.value.interior.type === "X1" &&
location.value.interior.value.type === "Parachain" &&
location.value.interior.value.value === PEOPLE_PARA_ID,
)!;
// Found it.
const messageToPeople = messages[0];
// Now that we know the XCM that will be executed on the people chain,
// we need to connect to it so we can estimate the fees.
const peopleClient = createClient(
withPolkadotSdkCompat(getWsProvider(PEOPLE_WS_URL)),
);
const peopleApi = peopleClient.getTypedApi(people);
// We're only dealing with version 4.
if (messageToPeople.type !== "V4") {
return;
}
// We get the delivery fees using the size of the forwarded xcm.
const deliveryFees = await ahApi.apis.XcmPaymentApi.query_delivery_fees(
XcmVersionedLocation.V4(PEOPLE_FROM_AH),
messageToPeople,
);
// Fees should be of the version we expect and fungible tokens, in particular, DOT.
if (
!deliveryFees.success ||
deliveryFees.value.type !== "V4" ||
deliveryFees.value.value[0].fun.type !== "Fungible"
) {
return;
}
// Local fees are execution + delivery.
const localFees = executionFees.value + deliveryFees.value.value[0].fun.value;
// Now we dry run on the destination.
const remoteDryRunResult = await peopleApi.apis.DryRunApi.dry_run_xcm(
XcmVersionedLocation.V4({
parents: 1,
interior: XcmV3Junctions.X1(XcmV3Junction.Parachain(ASSET_HUB_PARA_ID)),
}),
messageToPeople,
);
if (
!remoteDryRunResult.success ||
remoteDryRunResult.value.execution_result.type !== "Complete"
) {
return;
}
const remoteWeight =
await peopleApi.apis.XcmPaymentApi.query_xcm_weight(messageToPeople);
if (!remoteWeight.success) {
return;
}
// Remote fees are only execution.
const remoteFeesInDot =
await peopleApi.apis.XcmPaymentApi.query_weight_to_asset_fee(
remoteWeight.value,
XcmVersionedAssetId.V4(DOT_FROM_PARACHAINS),
);
if (!remoteFeesInDot.success) {
return;
}
return [localFees, remoteFeesInDot.value];
}
import { Builder } from '@paraspell/sdk';
import { getAliceSigner, stringify } from './utils';
const ASSET_HUB_WS_URL = 'ws://localhost:8000';
const PEOPLE_WS_URL = 'ws://localhost:8001';
const ALICE = '15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5';
const DOT_UNITS = 10_000_000_000n;
const tx = await Builder([ASSET_HUB_WS_URL, PEOPLE_WS_URL])
.from('AssetHubPolkadot')
.to('PeoplePolkadot')
.currency({ symbol: 'DOT', amount: 10n * DOT_UNITS })
.address(ALICE)
.senderAddress(ALICE)
.build();
const result = await tx.signAndSubmit(getAliceSigner());
console.dir(stringify(result));
import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy } from "@polkadot-labs/hdkd-helpers";
import type { PolkadotSigner } from "polkadot-api";
import { getPolkadotSigner } from "polkadot-api/signer";
// Gets a signer for the ALICE dev account.
export function getAliceSigner(): PolkadotSigner {
const entropy = mnemonicToEntropy(DEV_PHRASE);
const miniSecret = entropyToMiniSecret(entropy);
const derive = sr25519CreateDerive(miniSecret);
const hdkdKeyPair = derive("//Alice");
const aliceSigner = getPolkadotSigner(
hdkdKeyPair.publicKey,
"Sr25519",
hdkdKeyPair.sign,
);
return aliceSigner;
}
// Helper function to stringify an object using `converter` to also show bigints and binaries.
export function stringify(obj: any): string {
return JSON.stringify(obj, converter, 2);
}
// Helper function to convert bigints to strings and binaries to hex strings in objects.
function converter(_: string, value: any): string {
if (typeof value === "bigint") {
return value.toString();
} else if (typeof value === "object" && value.asHex !== undefined) {
return value.asHex();
} else {
return value;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment