Creating and interacting with an ERC-20 token on the Filecoin Virtual Machine using web3.js

Learn to create and interact with an ERC-20 token on the Filecoin Virtual Machine using web3.js. This comprehensive guide covers setting up your environment, deploying smart contracts, and executing token transfers, making web3 development accessible even for beginners.

Creating and interacting with an ERC-20 token on the Filecoin Virtual Machine using web3.js

Navigating the web3 space can be challenging if you're not very technical or lack experience. But don't worry, that's why we wrote this handy guide showing you how to navigate the Filecoin Virtual Machine using web3.js

Transfer between ERC-20 tokens and interact with our MetaMask wallet.

What You'll Need

About the Filecoin Virtual Machine:

Protocol Labs created the FVM to solve problems related to storage on the internet. It specifically addresses issues with centralized servers and enhances the overall programmability of the Filecoin storage network.

🎉
On March 14, 2023, it was launched with an ambitious mission to bring compatibility from the EVM to execute in its native runtime environment.
📒
💡
You can easily add the network by simply connecting your wallet to the desired Filecoin network. For this, you can use: https://chainid.network/.

Currently, there are two available networks: Filecoin Mainnet & Filecoin Calibration Testnet.

Filecoin Mainnet

This is the production network where all nodes are connected

Filecon Calibration Testnet

This network allows you to test features from the Filecoin Mainnet, but it should not be used to launch apps for production.

📒
For this tutorial, you can use the testnet or the mainnet. In the next steps, we'll show you how to get funds on the mainnet in case you want to deploy for production
📒
To receive funds, you need to convert your Ethereum address to the Filecoin standard format, which begins with 'f410'.
https://beryx.io/address_converter
Beryx.io Address Converter
📝
Example Ethereum address: 0x8B98F8Ff69d2A720120eD6C71A9Bc5072b8Eb46D to equals to f410frompr73j2ktsaeqo23drvg6fa4vy5ndn3uj3bqi

Fund your wallet on the Filecoin Virtual Machine testnet

To fund your wallet, you can receive funds through here

After converting your address to the f4 format, you need to paste it and then click on the "Get Tokens" button.

Filecoin Testnet Faucet

As a result, you'll receive your tFil tokens!

Tokens sent successfully

Fund your wallet on the Filecoin Virtual Machine mainnet

To send funds from any exchange, you need to enter the Filecoin address (f410) that you previously converted from your Ethereum address.

In our case we used Binance:

Withdrawing Filecoin from a Binance account to the mainnet

This is how a successful transaction looks

Deploying your ERC20 smart contract to the Filecoin Virtual Machine

To deploy our smart contract we need to go to:
https://remix.ethereum.org/

Compiling solidity code

After that, create a file named MyCoin.sol inside the 'contracts' folder and paste the code obtained from OpenZepellin.

ERC20 contract code on https://remix.ethereum.org/

Next, compile the code in the left sidebar in the green section marked in the image, and click on "Compile MyCoin.sol".

Compiling our ERC20 smart contract on https://remix.ethereum.org/

As a result, you'll see new options.

Scroll down and copy the: "ABI (Application Binary Interface)".

Copying the ABI from our smart contract.

Then, go to the section where we will deploy our smart contract!

Deploying our ERC20 smart contract

In the first dropdown "Environment" you need to change the option to:

"Injected Provide Metamask".

This will prompt Metamask later to deploy the contract.

💡
You can choose your desired network to deploy the contract!

In the second dropdown "Contract" you need to choose your contract. In this case, it's "MyToken"

In the "Deploy" section, you need to fill each text box with your Ethereum address.

Adding parameters to our smart contract

Then click on "Transact" to deploy the smart contract. The next window will display the fees associated with the transaction and other details on the "Hex Tab."

Click "Confirm".

Deploying our smart contract

After deploying your smart contract, you'll see functions associated with the code in the new fields displayed below.

Methods from smart contract displayed on Remix

You can see the smart contract deployed on the Filecoin mainnet network here

Recovering smart contract address

If the contract fails to render below or if you reload the page without saving your smart contract address, you can recover it through your MetaMask transaction history. Simply go to the "Activity" tab and click on the transaction.

MetaMask "Activity" tab.

Then go to the transaction and click "View on block explorer".

Metamask transaction details.

Finally, you will see this block explorer.

Transaction details related to our interaction with the smart contract.

To find your smart contract address, search for the value labelled "EthAddress"

Smart contract address recovered from our transaction.

Now go back to Remix, paste the address in the "At Address" section, and click the button. Now you should have your smart contract methods.

Recovering our smart contract address in Remix

Interacting with ERC20 tokens

First, we need to mint coins for our community. To do this, use the "mint" method and provide the following parameters:

to: The recipient's address.

amount: Specify the amount of tokens you wish to mint, denoted in wei.

Wei is the smallest denomination of ether, the cryptocurrency on the Ethereum network. It's important to express amounts in wei when interacting with Ethereum smart contracts, as it ensures precision in token transfers and calculations.

To convert your number to wei, you can use a method or tool like eth-converter

Mint Method from our Smart Contract

Now, a Metamask prompt will ask you to confirm the transaction.

Mint Method details

Now, to see your wallet balance, you need to add this token to your Metamask.

Metamask extension.

Click on the "Import tokens" option and paste the following values:

  • Token Contract Address
  • Token Symbol
Metamask import tokens section

Now you should be able to see your tokens displayed in Metamask.

ERC20 token displayed on Metamask.

Interacting with our ERC20 token with web3.js

Now that you have some practice with interacting with tokens, let's go over the flow using web3.js to interact with your smart contract.

In this scenario, you can transfer any ERC-20 token by simply providing the token address. This only works if the tokens adhere to the ERC-20 standard, which includes the transfer function for transferring tokens.

You can download the repository here.


In our project, we have two important files:

  1. script.js: Handles all the interactions involving web3.js, facilitating blockchain functionality. (Backend)
  2. index.html: Serves as the visually appealing interface where user interactions trigger significant actions and updates. (Frontend)

Workflow:

Workflow for our app interacting with ERC-20 and web3.js

script.js

This code sets up an App class that manages interactions with MetaMask for a web application. Here's a breakdown of the flow:

  1. DOMContentLoaded Event Listener:
    • document.addEventListener("DOMContentLoaded", () => app.initialize());
    • This ensures that app.initialize() is called when the DOM is fully loaded. This is where the setup begins.
  2. App Class:
    • Constructor: Initializes state variables and defines mappings for blockchain names based on chain IDs.
    • initialize(): Checks for MetaMask presence. If present, it sets up event listeners. Otherwise, it prompts the user to install MetaMask.
    • setupEventListeners(): Sets up event listeners for account connection, chain change events, and token sending.
    • handleAccountRequest(): Handles user interaction for connecting/disconnecting MetaMask.
    • connectToMetaMask(): Initiates a connection to MetaMask, requests permissions, retrieves accounts, and sets up web3Instance.
      In this function, we use:
      • window.ethereum.request({ method: 'wallet_requestPermissions', params: [{ eth_accounts: {} }] });
        • This code requests the user's permission to access their accounts. wallet_requestPermissions is a method that triggers a prompt in the user's wallet (like MetaMask) asking them to grant the dApp access to their account information.
      • window.ethereum.request({ method: 'eth_requestAccounts' });
        • This line requests the user to connect their accounts to the dApp. When this method is called, it prompts the user to select which accounts they want to provide to the dApp.
      • new Web3(window.ethereum);
        • This initializes a new instance of Web3 using the provider injected by MetaMask (window.ethereum). This instance will be used to interact with the blockchain.
    • disconnectMetaMask(): Clears account and instance data upon disconnect.
    • handleChainChanged(): Updates UI when the blockchain chain ID changes.
    • displayChainAndBlockInfo(): Retrieves current blockchain information (chain ID, latest block) and updates UI.
      • this.web3Instance.eth.getChainId()
        • Retrieves the current chain ID, which identifies the network the dApp is connected to (e.g., Mainnet, Testnet).
      • this.web3Instance.eth.getBlockNumber()
        • Retrieves the latest block number on the current blockchain.
      • accounts[0]
        • Refers to the first account in the list of accounts returned by the wallet.
    • getOwnedTokens(): Retrieves token balances for the connected wallet address and updates UI accordingly.
    • fetchTokenBalances(): Uses ERC20 ABI to fetch token balances for specified wallet addresses.
      • this.web3Instance.eth.Contract(erc20ABI, tokenAddress)
        • Initializes a new contract instance for an ERC-20 token using its ABI (Application Binary Interface) and the token's contract address.
      • tokenContract.methods.balanceOf(walletAddress).call()
        • Calls the balanceOf method on the contract to get the balance of a specified wallet address.
      • (this.web3Instance.utils.fromWei(balance, 'ether')).toString()
        • Converts the balance from Wei to Ether and then converts it again to a readable string.
    • sendTokens(): Handles token transfer functionality, validates inputs, and updates UI after a successful transaction.
    • transferTokens(): Transfers tokens using the ERC20 transfer method.
      • web3Instance.eth.Contract([ { "constant": false, "inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }], "name": "transfer", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" } ], tokenAddress);
        • Defines a contract interface for the transfer function of an ERC-20 token. The ABI here specifies the transfer method, its input parameters (_to and _value), and the type of function it is.
      • this.web3Instance.utils.toWei(amount, 'ether');
        • This converts an Ether amount to Wei. toWei is a utility function provided by web3.js to handle unit conversions.
      • tokenContract.methods.transfer(recipient, amountWei).send({ from: this.account });
        • This sends a specified amount of tokens to a recipient address by calling the transfer method on the ERC-20 token contract. The send function executes the transaction from the specified account.
    • populateDropdown(): Populates a dropdown menu with token options retrieved from MetaMask.
    • displayInstallMetaMaskWarning(): Displays a message prompting the user to install MetaMask if not detected.
    • updateUIAfterConnect(): Updates UI with account information and balance after a successful MetaMask connection.
    • updateUIAfterDisconnect(): Resets UI elements after MetaMask disconnection.
    • updateBlockchainInfo(): Updates UI with current blockchain information.
document.addEventListener("DOMContentLoaded", () => app.initialize());

class App {
  constructor() {
    this.web3Instance = null;
    this.account = null;
    this.requestPending = false;
    this.chainIdToNameMap = {
      314: { name: "Filecoin Mainnet", shortName: "Fil" },
      314159: { name: "Filecoin Testnet", shortName: "tFil" },
    };
  }

  initialize() {
    if (window.ethereum) {
      this.setupEventListeners();
    } else {
      this.displayInstallMetaMaskWarning();
    }
  }

  setupEventListeners() {
    document.getElementById('requestAccounts').addEventListener('click', () => this.handleAccountRequest());
    window.ethereum.on('chainChanged', () => this.handleChainChanged());
    document.getElementById('submitformSendTokens').addEventListener('click', (e) => this.sendTokens(e));
  }

  async handleAccountRequest() {
    const button = document.getElementById('requestAccounts');
    if (button.innerText === 'Connect to MetaMask' && !this.requestPending) {
      await this.connectToMetaMask();
    } else {
      this.disconnectMetaMask();
      this.clearOwnedTokens(); // Clear owned tokens when disconnecting
    }
  }

  async connectToMetaMask() {
    this.requestPending = true;
    try {
      await window.ethereum.request({ method: 'wallet_requestPermissions', params: [{ eth_accounts: {} }] });
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      this.web3Instance = new Web3(window.ethereum);
      this.account = accounts[0];
      this.updateUIAfterConnect(accounts[0]);
      await this.displayChainAndBlockInfo();
    } catch (error) {
      console.error(error);
    } finally {
      this.requestPending = false;
    }
  }

  disconnectMetaMask() {
    this.web3Instance = null;
    this.account = null;
    this.updateUIAfterDisconnect();
  }
  clearOwnedTokens() {
    const tokenInfo = document.getElementById('token-info');
    tokenInfo.style.display = 'none'; // Hide the "Owned Tokens" section
    const dropdown = document.getElementById('token-dropdown');
    dropdown.innerHTML = '<option value="" disabled selected>Select a token</option>'; // Clear the dropdown options
  }
  handleChainChanged() {
    this.displayChainAndBlockInfo();
  }

  async displayChainAndBlockInfo() {
    if (!this.web3Instance) return;
    const chainId = await this.web3Instance.eth.getChainId();
    const latestBlock = await this.web3Instance.eth.getBlockNumber();
    const blockchainName = this.chainIdToNameMap[chainId]?.name || "EVM Blockchain";
    this.updateBlockchainInfo(blockchainName, latestBlock);

    // Check if the user has owned tokens
    await this.getOwnedTokens(this.account);
}
async getOwnedTokens(walletAddress) {

    let tokens = await this.fetchTokenBalances(walletAddress);

    this.populateDropdown(tokens);

    // Show or hide the "Owned Tokens" section based on whether tokens are owned
    let tokenInfo = document.getElementById('token-info');
    if (tokens.length > 0) {
        tokenInfo.style.display = 'block';
    } else {
        tokenInfo.style.display = 'none';
    }
}

  async fetchTokenBalances(walletAddress) {
  
 
    const erc20ABI = [
      { "constant": true, "inputs": [{ "name": "_owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "balance", "type": "uint256" }], "type": "function" },
      { "constant": true, "inputs": [], "name": "name", "outputs": [{ "name": "", "type": "string" }], "type": "function" },
      { "constant": true, "inputs": [], "name": "symbol", "outputs": [{ "name": "", "type": "string" }], "type": "function" }
    ];

    const tokenContracts = ['0x84479f70535be87DC753943b0876FB737aa854e7'];
    let tokens = [];

    for (const tokenAddress of tokenContracts) {
      let tokenContract = new this.web3Instance.eth.Contract(erc20ABI, tokenAddress);
      let balance = await tokenContract.methods.balanceOf(walletAddress).call();

      if (balance > 0) {
        tokens.push({
          contractAddress: tokenAddress,
          name: "MTK",
          symbol: "MTK",
          balance: (this.web3Instance.utils.fromWei(balance, 'ether')).toString() 
        });
      }
    }
    return tokens;
  }

  async sendTokens(e) {
    e.preventDefault();
    let tokenDetails = document.getElementById("token-dropdown").value.split(",");
    let recipient = document.getElementById('recipient').value.trim();
    let amount = document.getElementById('tokenAmount').value.trim();

    if (!tokenDetails || !recipient || !amount) {
      alert('Please fill in all fields.');
      return;
    }

    if (!this.web3Instance) {
      alert('Please install MetaMask to use this feature.');
      return;
    }

    try {
      
      const [symbol,, tokenAddress] = tokenDetails;
      
      await this.transferTokens(tokenAddress, recipient, amount);
      let token=[];
      alert(`Successfully sent ${amount} tokens to ${recipient}`);
      token.push({ contractAddress: tokenAddress, name: "MTK", symbol: "MTK", balance: (localStorage.getItem(tokenAddress) - amount).toString() });
      this.populateDropdown(token);
    } catch (error) {
      console.error('Transaction failed:', error);
      alert('Transaction failed. Please check the console for details.');
    }
    document.getElementById('tokenAmount').value = "0";
    document.getElementById('recipient').value = "";
  }

  async transferTokens(tokenAddress, recipient, amount) {
    const tokenContract = new this.web3Instance.eth.Contract([
      {
        "constant": false,
        "inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
        "name": "transfer",
        "outputs": [{ "name": "", "type": "bool" }],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
      }
    ], tokenAddress);

    const amountWei = this.web3Instance.utils.toWei(amount, 'ether');
    await tokenContract.methods.transfer(recipient, amountWei).send({ from: this.account });
  }

  populateDropdown(tokens) {
    let dropdown = document.getElementById('token-dropdown');
    dropdown.innerHTML = '<option value="" disabled selected>Select a token</option>';
    tokens.forEach(token => {
      let option = document.createElement('option');
      option.value = `${token.symbol},${token.balance},${token.contractAddress}`;
      option.textContent = `${token.name} (${token.symbol}): ${token.balance}`;
      localStorage.setItem(token.contractAddress,token.balance);
      dropdown.appendChild(option);
    });
  }

  displayInstallMetaMaskWarning() {
    document.getElementById("requestAccounts").innerHTML = "<a href='https://metamask.io/download/' target='_blank' style='color: yellow;' onmouseover=\"this.style.color='white';\" onmouseout=\"this.style.color='yellow';\">Please install MetaMask</a>";
  }

  async updateUIAfterConnect(account) {
    const button = document.getElementById('requestAccounts');
    const chainId = await this.web3Instance.eth.getChainId();

    this.web3Instance.eth.getBalance(account).then(balance => {
      button.innerHTML = `Account: ${account}<br>Balance: ${this.web3Instance.utils.fromWei(balance, 'ether')!=0?this.web3Instance.utils.fromWei(balance, 'ether'):0} ${this.chainIdToNameMap[chainId].shortName}`;
      button.style.textAlign = 'left';
    });
  }

  updateUIAfterDisconnect() {
    const button = document.getElementById('requestAccounts');
    button.innerText = 'Connect to MetaMask';
    document.getElementById("blockchain").innerText = `Chain ID: 0`;
  }

  updateBlockchainInfo(blockchainName, latestBlock) {
    document.getElementById("blockchain").innerText = `Blockchain: ${blockchainName}`;
    document.getElementById("latestBlock").innerText = `Latest Block: ${latestBlock}`;
  }
}

const app = new App();

index.html

  • requestAccounts button: Initiates the connection to MetaMask.
  • submitformSendTokens button: Triggers the token sending process.
  • recipient input: Specifies the recipient address for token transfer.
  • tokenAmount input: Specifies the amount of tokens to be sent.
  • token-info section: Displays owned tokens.
  • token-dropdown dropdown: Lists owned tokens available for transfer.
  • blockchain element: Displays the blockchain name.
  • latestBlock element: Displays the latest block number

Deploying our solution

Now that we understand the solution, it's time to deploy. You need to go to Visual Studio Code, search for the index.html file, right-click on it, and then click on "Open with Live Server".

Testing our app using "Open with Live Server" in Visual Studio Code.

This article was based on the video tutorial below.

Crea e Interactúa con #Tokens #ERC20 en ‪@FilecoinProject‬ usando Web3.js [ESPAÑOL]

Learn more about web3.js

If you want to explore web3.js further, all functions with code examples can be found in the web3.js docs:

👉 Visit web3.js docs

👉 Follow @web3_js on Twitter for updates!

Still have questions? You can connect with other developers and get web3.js support in the ChainSafe Discord channel.

Just go to #web3js-general for all your web3.js questions. We hope to see you there soon! 😀 

👉 Join us on Discord!

About ChainSafe

ChainSafe is a leading blockchain research and development firm specializing in protocol engineering, cross-chain interoperability, and web3 gaming. Alongside its contributions to major ecosystems such as Ethereum, Polkadot, and Filecoin, ChainSafe creates solutions for developers across the web3 space utilizing expertise in gaminginteroperability, and decentralized storage. As part of its mission to build innovative products for users and improved tooling for developers, ChainSafe embodies an open-source and community-oriented ethos to advance the future of the internet.

Website | Twitter | Linkedin | GitHub | Discord | YouTube | Newsletter