Skip to content

Transfer Assets from Ethereum into Polkadot

This guide walks you through transferring tokens from Ethereum to Polkadot using the ParaSpell XCM SDK and Snowbridge. Snowbridge is a trustless, decentralized bridge integrated into the Polkadot protocol that enables secure asset transfers between the Ethereum and Polkadot ecosystems.

The following diagram shows the complete flow when bridging WETH from Ethereum to Polkadot:

sequenceDiagram
    participant User
    participant Ethereum
    participant Relayers
    participant Polkadot

    User->>Ethereum: 1. Approve & send tokens
    Ethereum->>Ethereum: Lock tokens
    Note over Relayers: ~30 min relay
    Relayers->>Polkadot: 2. Relay message
    Polkadot->>Polkadot: Verify & mint tokens
    Polkadot-->>User: 3. Tokens available

In this guide, you will:

  • Set up an Ethereum development environment with the ParaSpell SDK
  • Approve tokens for the Snowbridge Gateway contract
  • Build and execute a bridge transfer from Ethereum to Polkadot Hub
  • Monitor the transfer status

Before you begin, ensure you have the following:

  • A basic understanding of XCM.
  • Familiarity with JavaScript/TypeScript and Ethereum development.
  • Node.js v18 or higher and npm installed.
  • An Ethereum wallet with WETH tokens. See Prepare Tokens for Bridging for instructions to wrap ETH if needed.
  • A Polkadot account to receive the bridged assets.

To bridge ETH, you need to wrap it into WETH first. The WETH contract on Ethereum mainnet is:

0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

You can wrap ETH by:

  1. Visiting the WETH contract on Etherscan
  2. Connecting your wallet
  3. Calling the deposit function with the amount of ETH you want to wrap

Follow these steps to setup and initialize your project:

  1. Create the project folder using the following commands:
Terminal window
mkdir eth-to-polkadot-bridge && \
cd eth-to-polkadot-bridge
  1. Initialize the JavaScript project:
Terminal window
npm init -y && npm pkg set type=module

Install dev dependencies:

Terminal window
npm install --save-dev @types/node tsx typescript

Install the ParaSpell SDK, its Snowbridge extension, and viem, which the extension uses to sign Ethereum transactions:

Terminal window
npm install @paraspell/sdk@14.5.2 @paraspell/evm-snowbridge@14.5.2 polkadot-api@3.3.0 viem@2.57.16

Now add the following setup code to index.ts:

import { Builder, getBridgeStatus } from '@paraspell/sdk';
import { approveToken, getTokenBalance } from '@paraspell/evm-snowbridge';
import { createClient } from 'polkadot-api';
import { getWsProvider } from 'polkadot-api/ws';
import { createPublicClient, createWalletClient, formatUnits, http, type Hex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
// Ethereum mainnet RPC endpoint (use your own API key)
const ETH_RPC = 'https://eth-mainnet.g.alchemy.com/v2/INSERT_API_KEY';
// Polkadot Bridge Hub RPC endpoint, used to check the bridge status
const BRIDGE_HUB_RPC = 'wss://polkadot-bridge-hub-rpc.polkadot.io';
// WETH contract address on Ethereum mainnet
const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
// WETH has 18 decimals
const WETH_UNITS = 1_000_000_000_000_000_000n;
// Amount to bridge: 0.001 WETH (raw units as bigint)
const AMOUNT = WETH_UNITS / 1000n;
// Your Polkadot address to receive the bridged tokens (SS58 format)
const RECIPIENT_ADDRESS = 'INSERT_YOUR_POLKADOT_ADDRESS';
// Connect to Ethereum mainnet
// For a browser wallet (MetaMask), use custom(window.ethereum) as the transport
const walletClient = createWalletClient({
account: privateKeyToAccount('INSERT_PRIVATE_KEY' as Hex),
chain: mainnet,
transport: http(ETH_RPC),
});
const publicClient = createPublicClient({ chain: mainnet, transport: http(ETH_RPC) });

Replace the following placeholders:

  • INSERT_API_KEY: Your Ethereum RPC provider API key.
  • INSERT_PRIVATE_KEY: The private key of the Ethereum account holding your WETH, as a 0x-prefixed hex string.
  • INSERT_YOUR_POLKADOT_ADDRESS: The Polkadot account address (SS58 format) that will receive the bridged tokens on Polkadot Hub.

Before bridging ERC-20 tokens, you must approve the Snowbridge Gateway contract to spend your tokens. The @paraspell/evm-snowbridge package provides helper functions to check your balance and current allowance and to approve spending:

async function approveTokens() {
// Check current WETH balance and Snowbridge Gateway allowance
const { balance, gatewayAllowance } = await getTokenBalance(walletClient, 'WETH');
console.log(`Current WETH balance: ${formatUnits(balance, 18)}`);
if (balance === 0n) {
console.log('No WETH balance. Please wrap some ETH to WETH first.');
console.log(`WETH contract: ${WETH_ADDRESS}`);
process.exit(1);
}
if (gatewayAllowance >= AMOUNT) {
console.log('Gateway allowance already covers the transfer amount.');
return;
}
// Approve the Snowbridge Gateway contract to spend WETH
const approveTxHash = await approveToken(walletClient, AMOUNT, 'WETH');
console.log(`Approval transaction hash: ${approveTxHash}`);
// Wait for confirmation
await publicClient.waitForTransactionReceipt({ hash: approveTxHash });
console.log('Token approval confirmed!');
}
approveTokens();

Run the approval script:

Terminal window
npx tsx index.ts

You will see output confirming the token balance and approval similar to the following:

npx tsx index.ts Current WETH balance: 0.05 Approval transaction hash: 0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890 Token approval confirmed!

Now build and execute the bridge transfer from Ethereum to Polkadot Hub:

async function bridgeToPolkadot() {
console.log('Building bridge transaction...');
console.log(`Bridging ${formatUnits(AMOUNT, 18)} WETH to Polkadot Hub`);
console.log(`Recipient: ${RECIPIENT_ADDRESS}`);
// Build and execute the bridge transfer
// Note: 'AssetHubPolkadot' is the SDK identifier for Polkadot Hub
const builder = Builder()
.from('Ethereum')
.to('AssetHubPolkadot')
.currency({
symbol: 'WETH',
amount: AMOUNT,
})
.recipient(RECIPIENT_ADDRESS)
.sender(walletClient);
// Resolves once the Ethereum transaction is confirmed
const txHash = await builder.signAndSubmit();
await builder.disconnect();
console.log('Bridge transaction confirmed on Ethereum!');
console.log(`Transaction hash: ${txHash}`);
console.log('Transfer will arrive in approximately 30 minutes.');
}
bridgeToPolkadot();

Comment out the approveTokens() call and run the transfer:

Terminal window
npx tsx index.ts

The signAndSubmit() call resolves once the transaction is confirmed on Ethereum and has emitted the bridge message. You will see the Ethereum transaction hash in the terminal output similar to the following:

npx tsx index.ts Building bridge transaction... Bridging 0.001 WETH to Polkadot Hub Recipient: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY Bridge transaction confirmed on Ethereum! Transaction hash: 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 Transfer will arrive in approximately 30 minutes.

Bridge transfers from Ethereum to Polkadot take approximately 30 minutes. You can monitor the transfer status in several ways:

You can also check the bridge status programmatically. First, comment out the bridgeToPolkadot() call so the script doesn't send another transfer. Then add the following code to index.ts:

async function checkBridgeStatus() {
// Check the current status of the Snowbridge
const client = createClient(getWsProvider(BRIDGE_HUB_RPC));
const status = await getBridgeStatus(client);
console.log('Bridge Status:', status);
client.destroy();
}
checkBridgeStatus();

Run the script:

Terminal window
npx tsx index.ts

The script prints Bridge Status: Normal while the bridge is operating.

Snowbridge can bridge any ERC-20 token from Ethereum, but the destination chain must have that asset registered as a foreign asset to receive it.

You can query which assets are supported for a specific route using the ParaSpell SDK as follows:

import { getSupportedAssets } from '@paraspell/sdk';
// Get assets supported for Ethereum → Polkadot Hub transfers
const supportedAssets = getSupportedAssets('Ethereum', 'AssetHubPolkadot');
console.log('Supported assets:', supportedAssets.map(a => a.symbol));

Running this script will return a list of supported assets in the terminal similar to the following:

npx tsx get-supported-assets.ts Supported assets: [ 'sUSDe', 'WETH', 'PEPE', 'LDO', 'sUSDS', 'LBTC', 'EURC', 'SKY', 'USDC', 'MYTH', 'CGT2.0', 'TRAC', 'tBTC', 'wstETH', 'TONCOIN', 'DAI', 'SHIB', 'XRT', 'WBTC', 'TBTC', 'ETH', 'KILT', 'USDT', 'LINK', 'AAVE' ]

Common supported tokens on Polkadot Hub include:

TokenSymbolDescription
Wrapped EtherWETHEthereum's native token wrapped as ERC-20
Wrapped BitcoinWBTCBitcoin wrapped as an ERC-20 token
Shiba InuSHIBPopular ERC-20 meme token

To transfer to a different destination, change the .to() parameter and verify the asset is supported using the following code:

// Check if WETH is supported on Hydration
const hydrationAssets = getSupportedAssets('Ethereum', 'Hydration');
const wethSupported = hydrationAssets.some(a => a.symbol === 'WETH');
console.log('WETH supported on Hydration:', wethSupported);
if (wethSupported) {
const txHash = await Builder()
.from('Ethereum')
.to('Hydration')
.currency({ symbol: 'WETH', amount: AMOUNT })
.recipient(RECIPIENT_ADDRESS)
.sender(walletClient)
.signAndSubmit();
console.log(`Transaction hash: ${txHash}`);
}
npx tsx bridge-to-hydration.ts WETH supported on Hydration: true Transaction hash: 0x1234...abcd
Complete `index.ts` script
import { Builder, getBridgeStatus } from '@paraspell/sdk';
import { approveToken, getTokenBalance } from '@paraspell/evm-snowbridge';
import { createClient } from 'polkadot-api';
import { getWsProvider } from 'polkadot-api/ws';
import { createPublicClient, createWalletClient, formatUnits, http, type Hex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
// Ethereum mainnet RPC endpoint (use your own API key)
const ETH_RPC = 'https://eth-mainnet.g.alchemy.com/v2/INSERT_API_KEY';
// Polkadot Bridge Hub RPC endpoint, used to check the bridge status
const BRIDGE_HUB_RPC = 'wss://polkadot-bridge-hub-rpc.polkadot.io';
// WETH contract address on Ethereum mainnet
const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
// WETH has 18 decimals
const WETH_UNITS = 1_000_000_000_000_000_000n;
// Amount to bridge: 0.001 WETH (raw units as bigint)
const AMOUNT = WETH_UNITS / 1000n;
// Your Polkadot address to receive the bridged tokens (SS58 format)
const RECIPIENT_ADDRESS = 'INSERT_YOUR_POLKADOT_ADDRESS';
// Connect to Ethereum mainnet
// For a browser wallet (MetaMask), use custom(window.ethereum) as the transport
const walletClient = createWalletClient({
account: privateKeyToAccount('INSERT_PRIVATE_KEY' as Hex),
chain: mainnet,
transport: http(ETH_RPC),
});
const publicClient = createPublicClient({ chain: mainnet, transport: http(ETH_RPC) });
async function approveTokens() {
// Check current WETH balance and Snowbridge Gateway allowance
const { balance, gatewayAllowance } = await getTokenBalance(walletClient, 'WETH');
console.log(`Current WETH balance: ${formatUnits(balance, 18)}`);
if (balance === 0n) {
console.log('No WETH balance. Please wrap some ETH to WETH first.');
console.log(`WETH contract: ${WETH_ADDRESS}`);
process.exit(1);
}
if (gatewayAllowance >= AMOUNT) {
console.log('Gateway allowance already covers the transfer amount.');
return;
}
// Approve the Snowbridge Gateway contract to spend WETH
const approveTxHash = await approveToken(walletClient, AMOUNT, 'WETH');
console.log(`Approval transaction hash: ${approveTxHash}`);
// Wait for confirmation
await publicClient.waitForTransactionReceipt({ hash: approveTxHash });
console.log('Token approval confirmed!');
}
approveTokens();
async function bridgeToPolkadot() {
console.log('Building bridge transaction...');
console.log(`Bridging ${formatUnits(AMOUNT, 18)} WETH to Polkadot Hub`);
console.log(`Recipient: ${RECIPIENT_ADDRESS}`);
// Build and execute the bridge transfer
// Note: 'AssetHubPolkadot' is the SDK identifier for Polkadot Hub
const builder = Builder()
.from('Ethereum')
.to('AssetHubPolkadot')
.currency({
symbol: 'WETH',
amount: AMOUNT,
})
.recipient(RECIPIENT_ADDRESS)
.sender(walletClient);
// Resolves once the Ethereum transaction is confirmed
const txHash = await builder.signAndSubmit();
await builder.disconnect();
console.log('Bridge transaction confirmed on Ethereum!');
console.log(`Transaction hash: ${txHash}`);
console.log('Transfer will arrive in approximately 30 minutes.');
}
bridgeToPolkadot();
async function checkBridgeStatus() {
// Check the current status of the Snowbridge
const client = createClient(getWsProvider(BRIDGE_HUB_RPC));
const status = await getBridgeStatus(client);
console.log('Bridge Status:', status);
client.destroy();
}
checkBridgeStatus();

Last updated: