Teleport Tokens from Asset Hub to Bridge Hub
Introduction
This guide will walk you through the process of teleporting tokens from the Asset Hub to the Bridge Hub system parachain using the ParaSpell XCM SDK.
For development purposes, this guide will use the Paseo TestNet, so the teleport will be from Paseo's Asset Hub to Paseo's Bridge Hub.
You’ll learn how to:
- Build a teleport transaction.
- Perform a dry run to validate it.
- Verify the Existential Deposit (ED) requirement on the destination chain.
- Retrieve information regarding the transfer, along with fee estimates.
- Submit the transaction.
Prerequisites
Initialize Your Project
Create the project folder:
mkdir paraspell-teleport
cd paraspell-teleport
Initialize the JavaScript project:
Install the required dependencies:
bun add @paraspell/sdk polkadot-api @polkadot-labs/hdkd-helpers @polkadot-labs/hdkd
Now add the following setup code to index.ts
:
index.tsimport { Builder, hasDryRunSupport } from '@paraspell/sdk';
import {
entropyToMiniSecret,
mnemonicToEntropy,
ss58Address,
} from '@polkadot-labs/hdkd-helpers';
import { getPolkadotSigner } from 'polkadot-api/signer';
import { sr25519CreateDerive } from '@polkadot-labs/hdkd';
import { inspect } from 'util';
// DOT/PAS has 10 decimals
const PAS_UNITS = 10_000_000_000n;
const SEED_PHRASE =
'INSERT_YOUR_SEED_PHRASE';
// Create Sr25519 signer from mnemonic
function getSigner() {
const entropy = mnemonicToEntropy(SEED_PHRASE);
const miniSecret = entropyToMiniSecret(entropy);
const derive = sr25519CreateDerive(miniSecret);
const keyPair = derive('');
return getPolkadotSigner(keyPair.publicKey, 'Sr25519', keyPair.sign);
}
const RECIPIENT_ADDRESS = ss58Address(getSigner().publicKey);
const SENDER_ADDRESS = ss58Address(getSigner().publicKey);
Replace the INSERT_YOUR_SEED_PHRASE
with the seed phrase from your Polkadot development account.
Be sure to fund this account with some PAS tokens on Passeo's Asset Hub using the Polkadot Faucet.
Security Warning
Never commit your mnemonic phrase in production code. Use environment variables or secure key management systems.
Build a Teleport Transaction
The next step is to build the transaction that you intend to execute.
In this example, you will teleport 10 PAS tokens from Paseo's Asset Hub to Paseo's Bridge Hub system parachain.
Add the ParaSpell transaction code to your index.ts
file:
index.tsasync function teleport() {
const signer = getSigner();
const tx = await Builder()
.from('AssetHubPaseo')
.to('BridgeHubPaseo')
.currency({
symbol: 'PAS',
amount: 10n * PAS_UNITS, // 10 PAS
})
.address(RECIPIENT_ADDRESS)
.build();
console.log('Built transaction:', inspect(tx, { colors: true, depth: null }));
const result = await tx.signAndSubmit(signer);
console.log(inspect(result, { colors: true, depth: null }));
}
Do not execute it just yet. You will perform a dry run of this transaction first to ensure it works as expected.
Dry runs simulate the transaction without broadcasting it, allowing you to confirm success in advance.
Add the following dry run code to your index.ts
script:
index.tsasync function dryRunTeleport() {
if (!hasDryRunSupport('AssetHubPaseo')) {
console.log('Dry run is not supported on AssetHubPaseo.');
return;
}
const tx = await Builder()
.from('AssetHubPaseo')
.to('BridgeHubPaseo')
.currency({
symbol: 'PAS',
amount: 10n * PAS_UNITS,
})
.address(RECIPIENT_ADDRESS)
.senderAddress(SENDER_ADDRESS)
.dryRun();
console.log(inspect(tx, { colors: true, depth: null }));
}
dryRunTeleport();
Go ahead and run the script.
The result of the dry run will be similar to this:
bun run index.ts
{
failureReason: undefined,
failureChain: undefined,
origin: {
success: true,
fee: 17965000n,
weight: undefined,
forwardedXcms: [
{
type: 'V3',
value: {
parents: 1,
interior: { type: 'X1', value: { type: 'Parachain', value: 1002 } }
}
},
[
{
type: 'V3',
value: [
{
type: 'ReceiveTeleportedAsset',
value: [
{
id: {
type: 'Concrete',
value: {
parents: 1,
interior: { type: 'Here', value: undefined }
}
},
fun: { type: 'Fungible', value: 100000000000n }
}
]
},
{ type: 'ClearOrigin', value: undefined },
{
type: 'BuyExecution',
value: {
fees: {
id: {
type: 'Concrete',
value: {
parents: 1,
interior: { type: 'Here', value: undefined }
}
},
fun: { type: 'Fungible', value: 100000000000n }
},
weight_limit: { type: 'Unlimited', value: undefined }
}
},
{
type: 'DepositAsset',
value: {
assets: {
type: 'Wild',
value: { type: 'AllCounted', value: 1 }
},
beneficiary: {
parents: 0,
interior: {
type: 'X1',
value: {
type: 'AccountId32',
value: {
network: undefined,
id: FixedSizeBinary {
asText: [Function (anonymous)],
asHex: [Function (anonymous)],
asOpaqueHex: [Function (anonymous)],
asBytes: [Function (anonymous)],
asOpaqueBytes: [Function (anonymous)]
}
}
}
}
}
}
},
{
type: 'SetTopic',
value: FixedSizeBinary {
asText: [Function (anonymous)],
asHex: [Function (anonymous)],
asOpaqueHex: [Function (anonymous)],
asBytes: [Function (anonymous)],
asOpaqueBytes: [Function (anonymous)]
}
}
]
}
]
],
destParaId: 1002,
currency: 'PAS'
},
assetHub: undefined,
bridgeHub: undefined,
destination: {
success: true,
fee: 17965000n,
weight: { refTime: 164770000n, proofSize: 3593n },
forwardedXcms: [],
destParaId: undefined,
currency: 'PAS'
},
hops: []
}
Verify the Existential Deposit
Check if the recipient account meets the Existential Deposit (ED) requirement before sending by using verifyEdOnDestination
:
index.tsasync function verifyED() {
const isValid = await Builder()
.from('AssetHubPaseo')
.to('BridgeHubPaseo')
.currency({
symbol: 'PAS',
amount: 10n * PAS_UNITS,
})
.address(RECIPIENT_ADDRESS)
.senderAddress(SENDER_ADDRESS)
.verifyEdOnDestination();
console.log(`ED verification ${isValid ? 'successful' : 'failed'}.`);
}
verifyED();
Execute the code by running:
After that, you will get output confirming the ED:
bun run index.ts
...
ED verification successful.
Get Transfer Info and Fee Estimates
Before sending an XCM transaction, it is helpful to estimate the fees associated with executing and delivering the cross-chain message.
ParaSpell has a helpful function for this: getTransferInfo()
. This function returns an estimate of the associated XCM fees, along with the account's balance before and after the fees are paid.
index.tsasync function XcmTransferInfo() {
const info = await Builder()
.from('AssetHubPaseo')
.to('BridgeHubPaseo')
.currency({
symbol: 'PAS',
amount: 10n * PAS_UNITS,
})
.address(RECIPIENT_ADDRESS)
.senderAddress(SENDER_ADDRESS)
.getTransferInfo();
console.log('Transfer Info:', info);
}
XcmTransferInfo();
Go ahead and execute the script:
You should be able to see all the information for your transfer:
bun run index.ts
...
Transfer Info: {
chain: {
origin: 'AssetHubPaseo',
destination: 'BridgeHubPaseo',
ecosystem: 'PAS'
},
origin: {
selectedCurrency: {
sufficient: true,
balance: 9899002813408n,
balanceAfter: 9799002813408n,
currencySymbol: 'PAS',
existentialDeposit: 100000000n
},
xcmFee: {
sufficient: true,
fee: 17965000n,
balance: 9899002813408n,
balanceAfter: 9898984848408n,
currencySymbol: 'PAS'
}
},
assetHub: undefined,
bridgeHub: undefined,
hops: [],
destination: {
receivedCurrency: {
sufficient: true,
receivedAmount: 99982035000n,
balance: 0n,
balanceAfter: 99982035000n,
currencySymbol: 'PAS',
existentialDeposit: 1000000000n
},
xcmFee: {
fee: 17965000n,
balance: 0n,
balanceAfter: 99982035000n,
currencySymbol: 'PAS'
}
}
}
Now that you have:
- Completed a successful dry run of the transaction
- Verified the existential deposit on the recipient account
- Obtained an estimate of the associated XCM fees
Now you can execute the teleport function by adding the following statement:
Add the following code:
And execute your teleport:
Your teleport
function will submit the transaction, and you will get the following output:
bun run index.ts
...
Built transaction: {
getPaymentInfo: [AsyncFunction: getPaymentInfo],
getEstimatedFees: [AsyncFunction: getEstimatedFees],
decodedCall: {
type: 'PolkadotXcm',
value: {
type: 'limited_teleport_assets',
value: {
dest: {
type: 'V5',
value: {
parents: 1,
interior: { type: 'X1', value: { type: 'Parachain', value: 1002 } }
}
},
beneficiary: {
type: 'V5',
value: {
parents: 0,
interior: {
type: 'X1',
value: {
type: 'AccountId32',
value: {
network: undefined,
id: FixedSizeBinary {
asText: [Function (anonymous)],
asHex: [Function (anonymous)],
asOpaqueHex: [Function (anonymous)],
asBytes: [Function (anonymous)],
asOpaqueBytes: [Function (anonymous)]
}
}
}
}
}
},
assets: {
type: 'V5',
value: [
{
id: { parents: 1, interior: { type: 'Here', value: null } },
fun: { type: 'Fungible', value: 100000000000n }
}
]
},
fee_asset_item: 0,
weight_limit: { type: 'Unlimited' }
}
}
},
getEncodedData: [Function: getEncodedData],
sign: [Function: sign],
signSubmitAndWatch: [Function: signSubmitAndWatch],
signAndSubmit: [Function: signAndSubmit]
}
Once the transaction is successfully included in a block, you will see the recipient's account balance updated, and you will receive output similar to the one below.
Successful Transaction Submission
This output will be returned once the transaction has been successfully included in a block.
...
{
txHash: '0x6fbecc0b284adcff46ab39872659c2567395c865adef5f8cbea72f25b6042609',
block: {
index: 2,
number: 2524809,
hash: '0xa39a96d5921402c6e8f67e48b8395d6b21382c72d4d30f8497a0e9f890bc0d4c'
},
ok: true,
events: [
{
type: 'Balances',
value: {
type: 'Withdraw',
value: {
who: '15DMtB5BDCJqw4uZtByTWXGqViAVx7XjRsxWbTH5tfrHLe8j',
amount: 15668864n
}
},
topics: []
},
{
type: 'Balances',
value: {
type: 'Burned',
value: {
who: '15DMtB5BDCJqw4uZtByTWXGqViAVx7XjRsxWbTH5tfrHLe8j',
amount: 100000000000n
}
},
topics: []
},
{
type: 'PolkadotXcm',
value: {
type: 'Attempted',
value: {
outcome: {
type: 'Complete',
value: { used: { ref_time: 190990000n, proof_size: 3593n } }
}
}
},
topics: []
},
{
type: 'Balances',
value: {
type: 'Burned',
value: {
who: '15DMtB5BDCJqw4uZtByTWXGqViAVx7XjRsxWbTH5tfrHLe8j',
amount: 304850000n
}
},
topics: []
},
{
type: 'Balances',
value: {
type: 'Minted',
value: {
who: '14xmwinmCEz6oRrFdczHKqHgWNMiCysE2KrA4jXXAAM1Eogk',
amount: 304850000n
}
},
topics: []
},
{
type: 'PolkadotXcm',
value: {
type: 'FeesPaid',
value: {
paying: {
parents: 0,
interior: {
type: 'X1',
value: {
type: 'AccountId32',
value: {
network: { type: 'Polkadot', value: undefined },
id: FixedSizeBinary {
asText: [Function (anonymous)],
asHex: [Function (anonymous)],
asOpaqueHex: [Function (anonymous)],
asBytes: [Function (anonymous)],
asOpaqueBytes: [Function (anonymous)]
}
}
}
}
},
fees: [
{
id: {
parents: 1,
interior: { type: 'Here', value: undefined }
},
fun: { type: 'Fungible', value: 304850000n }
}
]
}
},
topics: []
},
{
type: 'XcmpQueue',
value: {
type: 'XcmpMessageSent',
value: {
message_hash: FixedSizeBinary {
asText: [Function (anonymous)],
asHex: [Function (anonymous)],
asOpaqueHex: [Function (anonymous)],
asBytes: [Function (anonymous)],
asOpaqueBytes: [Function (anonymous)]
}
}
},
topics: []
},
{
type: 'PolkadotXcm',
value: {
type: 'Sent',
value: {
origin: {
parents: 0,
interior: {
type: 'X1',
value: {
type: 'AccountId32',
value: {
network: { type: 'Polkadot', value: undefined },
id: FixedSizeBinary {
asText: [Function (anonymous)],
asHex: [Function (anonymous)],
asOpaqueHex: [Function (anonymous)],
asBytes: [Function (anonymous)],
asOpaqueBytes: [Function (anonymous)]
}
}
}
}
},
destination: {
parents: 1,
interior: { type: 'X1', value: { type: 'Parachain', value: 1002 } }
},
message: [
{
type: 'ReceiveTeleportedAsset',
value: [
{
id: {
parents: 1,
interior: { type: 'Here', value: undefined }
},
fun: { type: 'Fungible', value: 100000000000n }
}
]
},
{ type: 'ClearOrigin', value: undefined },
{
type: 'BuyExecution',
value: {
fees: {
id: {
parents: 1,
interior: { type: 'Here', value: undefined }
},
fun: { type: 'Fungible', value: 100000000000n }
},
weight_limit: { type: 'Unlimited', value: undefined }
}
},
{
type: 'DepositAsset',
value: {
assets: {
type: 'Wild',
value: { type: 'AllCounted', value: 1 }
},
beneficiary: {
parents: 0,
interior: {
type: 'X1',
value: {
type: 'AccountId32',
value: {
network: undefined,
id: FixedSizeBinary {
asText: [Function (anonymous)],
asHex: [Function (anonymous)],
asOpaqueHex: [Function (anonymous)],
asBytes: [Function (anonymous)],
asOpaqueBytes: [Function (anonymous)]
}
}
}
}
}
}
}
],
message_id: FixedSizeBinary {
asText: [Function (anonymous)],
asHex: [Function (anonymous)],
asOpaqueHex: [Function (anonymous)],
asBytes: [Function (anonymous)],
asOpaqueBytes: [Function (anonymous)]
}
}
},
topics: []
},
{
type: 'Balances',
value: {
type: 'Deposit',
value: {
who: '13UVJyLgBASGhE2ok3TvxUfaQBGUt88JCcdYjHvUhvQkFTTx',
amount: 15668864n
}
},
topics: []
},
{
type: 'TransactionPayment',
value: {
type: 'TransactionFeePaid',
value: {
who: '15DMtB5BDCJqw4uZtByTWXGqViAVx7XjRsxWbTH5tfrHLe8j',
actual_fee: 15668864n,
tip: 0n
}
},
topics: []
},
{
type: 'System',
value: {
type: 'ExtrinsicSuccess',
value: {
dispatch_info: {
weight: { ref_time: 952851000n, proof_size: 13382n },
class: { type: 'Normal', value: undefined },
pays_fee: { type: 'Yes', value: undefined }
}
}
},
topics: []
}
]
}
After executing the teleport, check the account balance on Polkadot.js Apps for Paseo's Asset Hub and Paseo's Bridge Hub.
You should see:
- The recipient account now has 10 more PAS tokens.
- The sender account has the transfer amount (10 PAS) + the fees amount debited from their account balance.
You have now successfully created and sent a cross-chain transfer using the ParaSpell XCM SDK!
Next Steps
Last update:
August 27, 2025
|
Created:
August 27, 2025