Skip to content

Ethers.js

Introduction

Ethers.js is a lightweight library that enables interaction with Ethereum Virtual Machine (EVM)-compatible blockchains through JavaScript. Ethers is widely used as a toolkit to establish connections and read and write blockchain data. This article demonstrates using Ethers.js to interact and deploy smart contracts to Asset Hub.

Set Up the Project

To start working with Ethers.js, create a new folder and initialize your project by running the following commands in your terminal:

mkdir ethers-project
cd ethers-project
npm init -y

Install Dependencies

Next, run the following command to install the Ethers.js library:

npm install ethers

Set Up the Ethers.js Provider

To interact with the Asset Hub, you must set up an Ethers.js provider. This provider connects to a blockchain node, allowing you to query blockchain data and interact with smart contracts. In the root of your project, create a file named connectToProvider.js and add the following code:

connectToProvider.js
const { JsonRpcProvider } = require('ethers');

const createProvider = (rpcUrl, chainId, chainName) => {
  const provider = new JsonRpcProvider(rpcUrl, {
    chainId: chainId,
    name: chainName,
  });

  return provider;
};

const PROVIDER_RPC = {
  rpc: 'INSERT_RPC_URL',
  chainId: 'INSERT_CHAIN_ID',
  name: 'INSERT_CHAIN_NAME',
};

createProvider(PROVIDER_RPC.rpc, PROVIDER_RPC.chainId, PROVIDER_RPC.name);

Note

Replace INSERT_RPC_URL, INSERT_CHAIN_ID, and INSERT_CHAIN_NAME with the appropriate values. For example, to connect to Westend Asset Hub's Ethereum RPC instance, you can use the following parameters:

const PROVIDER_RPC = {
    rpc: 'https://westend-asset-hub-eth-rpc.polkadot.io',
    chainId: 420420421,
    name: 'westend-asset-hub'
};

With the Provider set up, you can start querying the blockchain. For instance, to fetch the latest block number:

Fetch Last Block code
fetchLastBlock.js
const { JsonRpcProvider } = require('ethers');

const createProvider = (rpcUrl, chainId, chainName) => {
  const provider = new JsonRpcProvider(rpcUrl, {
    chainId: chainId,
    name: chainName,
  });

  return provider;
};

const PROVIDER_RPC = {
  rpc: 'https://westend-asset-hub-eth-rpc.polkadot.io',
  chainId: 420420421,
  name: 'westend-asset-hub',
};

const main = async () => {
  try {
    const provider = createProvider(
      PROVIDER_RPC.rpc,
      PROVIDER_RPC.chainId,
      PROVIDER_RPC.name
    );
    const latestBlock = await provider.getBlockNumber();
    console.log(`Latest block: ${latestBlock}`);
  } catch (error) {
    console.error('Error connecting to Asset Hub: ' + error.message);
  }
};

main();

Compile Contracts

The revive compiler transforms Solidity smart contracts into PolkaVM bytecode for deployment on Asset Hub. Revive's Ethereum RPC interface allows you to use familiar tools like Ethers.js and MetaMask to interact with contracts.

Install the Revive Library

The @parity/revive library will compile your Solidity code for deployment on Asset Hub. Run the following command in your terminal to install the library:

npm install --save-dev @parity/revive 

Sample Storage.sol Smart Contract

This example demonstrates compiling a Storage.sol Solidity contract for deployment to Asset Hub. The contract's functionality stores a number and permits users to update it with a new value.

storage.sol
//SPDX-License-Identifier: MIT

// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
pragma solidity ^0.8.9;

contract Storage {
    // Public state variable to store a number
    uint256 public storedNumber;

    /**
    * Updates the stored number.
    *
    * The `public` modifier allows anyone to call this function.
    *
    * @param _newNumber - The new value to store.
    */
    function setNumber(uint256 _newNumber) public {
        storedNumber = _newNumber;
    }
}

Compile the Smart Contract

To compile this contract, use the following script:

compile.js
const { compile } = require('@parity/revive');
const { readFileSync, writeFileSync } = require('fs');
const { basename, join } = require('path');

const compileContract = async (solidityFilePath, outputDir) => {
  try {
    // Read the Solidity file
    const source = readFileSync(solidityFilePath, 'utf8');

    // Construct the input object for the compiler
    const input = {
      [basename(solidityFilePath)]: { content: source },
    };

    console.log(`Compiling contract: ${basename(solidityFilePath)}...`);

    // Compile the contract
    const out = await compile(input);

    for (const contracts of Object.values(out.contracts)) {
      for (const [name, contract] of Object.entries(contracts)) {
        console.log(`Compiled contract: ${name}`);

        // Write the ABI
        const abiPath = join(outputDir, `${name}.json`);
        writeFileSync(abiPath, JSON.stringify(contract.abi, null, 2));
        console.log(`ABI saved to ${abiPath}`);

        // Write the bytecode
        const bytecodePath = join(outputDir, `${name}.polkavm`);
        writeFileSync(
          bytecodePath,
          Buffer.from(contract.evm.bytecode.object, 'hex')
        );
        console.log(`Bytecode saved to ${bytecodePath}`);
      }
    }
  } catch (error) {
    console.error('Error compiling contracts:', error);
  }
};

const solidityFilePath = './Storage.sol';
const outputDir = '.';

compileContract(solidityFilePath, outputDir);

Note

The script above is tailored to the Storage.sol contract. It can be adjusted for other contracts by changing the file name or modifying the ABI and bytecode paths.

After executing the script, the Solidity contract will be compiled into the required polkavm bytecode format. The ABI and bytecode will be saved into files with .json and .polkavm extensions, respectively. You can now proceed with deploying the contract to the Asset Hub network, as outlined in the next section.

Deploy the Compiled Contract

To deploy your compiled contract to Asset Hub, you'll need a wallet with a private key to sign the deployment transaction.

You can create a deploy.js script in the root of your project to achieve this. The deployment script can be divided into key components:

  1. First, set up the required imports and utilities:

    deploy.js
    const { join } = require('path');
    const { ethers, JsonRpcProvider } = require('ethers');
    
    const codegenDir = join(__dirname);
    
  2. Create a provider to connect to the Asset Hub network:

    deploy.js
    // Creates an Ethereum provider with specified RPC URL and chain details
    const createProvider = (rpcUrl, chainId, chainName) => {
      const provider = new JsonRpcProvider(rpcUrl, {
        chainId: chainId,
        name: chainName,
      });
      return provider;
    };
    
  3. Set up functions to read contract artifacts:

    deploy.js
    // Reads and parses the ABI file for a given contract
    const getAbi = (contractName) => {
      try {
        return JSON.parse(
          readFileSync(join(codegenDir, `${contractName}.json`), 'utf8')
        );
      } catch (error) {
        console.error(
          `Could not find ABI for contract ${contractName}:`,
          error.message
        );
        throw error;
      }
    };
    
    // Reads the compiled bytecode for a given contract
    const getByteCode = (contractName) => {
      try {
        return `0x${readFileSync(
          join(codegenDir, `${contractName}.polkavm`)
        ).toString('hex')}`;
      } catch (error) {
        console.error(
          `Could not find bytecode for contract ${contractName}:`,
          error.message
        );
        throw error;
    
  4. Create the main deployment function:

    deploy.js
    };
    
    const deployContract = async (contractName, mnemonic, providerConfig) => {
      console.log(`Deploying ${contractName}...`);
    
      try {
        // Step 1: Set up provider and wallet
        const provider = createProvider(
          providerConfig.rpc,
          providerConfig.chainId,
          providerConfig.name
        );
        const walletMnemonic = ethers.Wallet.fromPhrase(mnemonic);
        const wallet = walletMnemonic.connect(provider);
    
        // Step 2: Create and deploy the contract
        const factory = new ethers.ContractFactory(
          getAbi(contractName),
          getByteCode(contractName),
          wallet
        );
        const contract = await factory.deploy();
        await contract.waitForDeployment();
    
        // Step 3: Save deployment information
        const address = await contract.getAddress();
        console.log(`Contract ${contractName} deployed at: ${address}`);
    
        const addressesFile = join(codegenDir, 'contract-address.json');
        const addresses = existsSync(addressesFile)
          ? JSON.parse(readFileSync(addressesFile, 'utf8'))
          : {};
        addresses[contractName] = address;
        writeFileSync(addressesFile, JSON.stringify(addresses, null, 2), 'utf8');
      } catch (error) {
        console.error(`Failed to deploy contract ${contractName}:`, error);
    
  5. Configure and execute the deployment:

    deploy.js
    };
    
    const providerConfig = {
      rpc: 'https://westend-asset-hub-eth-rpc.polkadot.io',
      chainId: 420420421,
      name: 'westend-asset-hub',
    };
    
    const mnemonic = 'INSERT_MNEMONIC';
    

    Note

    Ensure to replace the INSERT_MNEMONIC placeholder with your actual mnemonic.

Here's the complete deployment script combining all the components above:

deploy.js
deploy.js
const { writeFileSync, existsSync, readFileSync } = require('fs');
const { join } = require('path');
const { ethers, JsonRpcProvider } = require('ethers');

const codegenDir = join(__dirname);

// Creates an Ethereum provider with specified RPC URL and chain details
const createProvider = (rpcUrl, chainId, chainName) => {
  const provider = new JsonRpcProvider(rpcUrl, {
    chainId: chainId,
    name: chainName,
  });
  return provider;
};

// Reads and parses the ABI file for a given contract
const getAbi = (contractName) => {
  try {
    return JSON.parse(
      readFileSync(join(codegenDir, `${contractName}.json`), 'utf8')
    );
  } catch (error) {
    console.error(
      `Could not find ABI for contract ${contractName}:`,
      error.message
    );
    throw error;
  }
};

// Reads the compiled bytecode for a given contract
const getByteCode = (contractName) => {
  try {
    return `0x${readFileSync(
      join(codegenDir, `${contractName}.polkavm`)
    ).toString('hex')}`;
  } catch (error) {
    console.error(
      `Could not find bytecode for contract ${contractName}:`,
      error.message
    );
    throw error;
  }
};

const deployContract = async (contractName, mnemonic, providerConfig) => {
  console.log(`Deploying ${contractName}...`);

  try {
    // Step 1: Set up provider and wallet
    const provider = createProvider(
      providerConfig.rpc,
      providerConfig.chainId,
      providerConfig.name
    );
    const walletMnemonic = ethers.Wallet.fromPhrase(mnemonic);
    const wallet = walletMnemonic.connect(provider);

    // Step 2: Create and deploy the contract
    const factory = new ethers.ContractFactory(
      getAbi(contractName),
      getByteCode(contractName),
      wallet
    );
    const contract = await factory.deploy();
    await contract.waitForDeployment();

    // Step 3: Save deployment information
    const address = await contract.getAddress();
    console.log(`Contract ${contractName} deployed at: ${address}`);

    const addressesFile = join(codegenDir, 'contract-address.json');
    const addresses = existsSync(addressesFile)
      ? JSON.parse(readFileSync(addressesFile, 'utf8'))
      : {};
    addresses[contractName] = address;
    writeFileSync(addressesFile, JSON.stringify(addresses, null, 2), 'utf8');
  } catch (error) {
    console.error(`Failed to deploy contract ${contractName}:`, error);
  }
};

const providerConfig = {
  rpc: 'https://westend-asset-hub-eth-rpc.polkadot.io',
  chainId: 420420421,
  name: 'westend-asset-hub',
};

const mnemonic = 'INSERT_MNEMONIC';

deployContract('Storage', mnemonic, providerConfig);

After running this script, your contract will be deployed to Asset Hub, and its address will be saved in contract-address.json within your project directory. You can use this address for future contract interactions.

Interact with the Contract

Once the contract is deployed, you can interact with it by calling its functions. For example, to set a number, read it and then modify that number by its double, you can create a file named checkStorage.js in the root of your project and add the following code:

checkStorage.js
const { ethers } = require('ethers');
const { readFileSync } = require('fs');
const { join } = require('path');

const createProvider = (providerConfig) => {
  return new ethers.JsonRpcProvider(providerConfig.rpc, {
    chainId: providerConfig.chainId,
    name: providerConfig.name,
  });
};

const createWallet = (mnemonic, provider) => {
  return ethers.Wallet.fromPhrase(mnemonic).connect(provider);
};

const loadContractAbi = (contractName, directory = __dirname) => {
  const contractPath = join(directory, `${contractName}.json`);
  const contractJson = JSON.parse(readFileSync(contractPath, 'utf8'));
  return contractJson.abi || contractJson; // Depending on JSON structure
};

const createContract = (contractAddress, abi, wallet) => {
  return new ethers.Contract(contractAddress, abi, wallet);
};

const interactWithStorageContract = async (
  contractName,
  contractAddress,
  mnemonic,
  providerConfig,
  numberToSet
) => {
  try {
    console.log(`Setting new number in Storage contract: ${numberToSet}`);

    // Create provider and wallet
    const provider = createProvider(providerConfig);
    const wallet = createWallet(mnemonic, provider);

    // Load the contract ABI and create the contract instance
    const abi = loadContractAbi(contractName);
    const contract = createContract(contractAddress, abi, wallet);

    // Send a transaction to set the stored number
    const tx1 = await contract.setNumber(numberToSet);
    await tx1.wait(); // Wait for the transaction to be mined
    console.log(`Number successfully set to ${numberToSet}`);

    // Retrieve the updated number
    const storedNumber = await contract.storedNumber();
    console.log(`Retrieved stored number:`, storedNumber.toString());

    // Send a transaction to set the stored number
    const tx2 = await contract.setNumber(numberToSet * 2);
    await tx2.wait(); // Wait for the transaction to be mined
    console.log(`Number successfully set to ${numberToSet * 2}`);

    // Retrieve the updated number
    const updatedNumber = await contract.storedNumber();
    console.log(`Retrieved stored number:`, updatedNumber.toString());
  } catch (error) {
    console.error('Error interacting with Storage contract:', error.message);
  }
};

const providerConfig = {
  name: 'asset-hub-smart-contracts',
  rpc: 'https://westend-asset-hub-eth-rpc.polkadot.io',
  chainId: 420420421,
};

const mnemonic = 'INSERT_MNEMONIC';
const contractName = 'Storage';
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
const newNumber = 42;

interactWithStorageContract(
  contractName,
  contractAddress,
  mnemonic,
  providerConfig,
  newNumber
);

Ensure you replace the INSERT_MNEMONIC, INSERT_CONTRACT_ADDRESS, and INSERT_ADDRESS_TO_CHECK placeholders with actual values. Also, ensure the contract ABI file (Storage.json) is correctly referenced.

Where to Go Next

Now that you have the foundational knowledge to use Ethers.js with Asset Hub, you can:

  • Dive into Ethers.js utilities - discover additional Ethers.js features, such as wallet management, signing messages, etc
  • Implement batch transactions - use Ethers.js to execute batch transactions for efficient multi-step contract interactions
  • Build scalable applications - combine Ethers.js with frameworks like Next.js or Node.js to create full-stack decentralized applications (dApps)
Last update: March 6, 2025
| Created: March 6, 2025