Skip to main content

Substrate To EVM Token Transfer Example


In the following example, we will use the TESTNET environment to perform a cross-chain ERC-20 transfer with the Goerli Phala gPHA token. The transfer will be initiated on the Substrate-side via the Rococo-Phala testnet and received on the EVM-side via the Goerli Ethereum testnet.

EVM-to-Substrate Token Transfer Example

This is an example script that demonstrates the functionality of the Sygma SDK and the wider Sygma ecosystem of bridges, fee handlers, and relayers. The script showcases a Substrate asset transfer between Substrate and EVM using the Sygma SDK. The complete example can be found in this repo.


Before running the script, ensure that you have the following:

  • Node.js installed on your machine
  • Yarn (version 3.4.1 or higher)
  • Access to a custom Substrate WSS endpoint
  • A wallet funded with gPHA tokens from the Sygma faucet

We make use of the dotenv module to manage Substrate's private mnemonics with environment variables. Please note that accidentally committing a .env file containing private mnemonics to a wallet with real funds, onto GitHub, could result in the complete loss of your funds. Never expose your private keys.

Getting Started

  1. Clone the repository

Clone the sygma-sdk repository into a directory of your choice, and then cd into the folder:

git clone
cd sygma-sdk/
  1. Install dependencies

Install the project dependencies by running:

yarn install
  1. Build the SDK

Build the SDK by running the following command:

yarn sdk:build
  1. Usage

To send an ERC-20 example transfer from EVM to Substrate, cd into the example folder examples/substrate-to-evm-fungible-transfer and run:

cd examples/substrate-to-evm-fungible-transfer
yarn run transfer

The example will use @polkadot/keyring in conjunction with the sygma-sdk to create a transfer from Rococo-Phala to Goerli with the PHA token. It will be received on Goerli as a gPHA token.

Replace the placeholder values in the script with your own Substrate wallet mnemonic and destination EVM address.

Script Functionality

This example script performs the following steps:

  • Initializes the SDK by importing the required packages and defining the constants for the script.
import { Keyring } from "@polkadot/keyring";
import { ApiPromise, WsProvider } from "@polkadot/api";
import { cryptoWaitReady } from "@polkadot/util-crypto";
import { Environment, Substrate } from "@buildwithsygma/sygma-sdk-core";

const { SubstrateAssetTransfer } = Substrate;
const GOERLI_CHAIN_ID = 5;
const RESOURCE_ID = "0x0000000000000000000000000000000000000000000000000000000000001000";
const recipient = "0xD31E89feccCf6f2DE10EaC92ADffF48D802b695C";
  • Configures the dotenv module and sets the MNEMONIC as a value to be pulled from the .env file.
import dotenv from "dotenv";


const MNEMONIC = process.env.PRIVATE_MNEMONIC;

if (!MNEMONIC) {
throw new Error("Missing environment variable: PRIVATE_MNEMONIC");
  • Defines the main Substrate transfer function, including the connection to the blockchain using a WebSocket provider, initializing the asset transfer instance, and setting up the keyring and account from the mnemonic phrase.
const substrateTransfer = async (): Promise<void> => {
const keyring = new Keyring({ type: "sr25519" });
await cryptoWaitReady();
const account = keyring.addFromUri(MNEMONIC as string);
const wsProvider = new WsProvider("wss://");
const api = await ApiPromise.create({ provider: wsProvider });
const assetTransfer = new SubstrateAssetTransfer();
await assetTransfer.init(api, Environment.TESTNET);
  • Constructs a transfer object that calculates the fee, then builds, signs, and sends the transaction.
const transfer = assetTransfer.createFungibleTransfer(
const fee = await assetTransfer.getFee(transfer);
const transferTx = assetTransfer.buildTransferTransaction(transfer, fee);
const unsub = await transferTx.signAndSend(account, ({ status }) => {
  • Logs the current status of the transaction, and if it's included in a block or finalized, outputs the respective block hash.
const unsub = await transferTx.signAndSend(account, ({ status }) => {
console.log(`Current status is ${status.toString()}`);
if (status.isInBlock) {
console.log(`Transaction included at blockHash ${status.asInBlock.toString()}`);
} else if (status.isFinalized) {
console.log(`Transaction finalized at blockHash ${status.asFinalized.toString()}`);