Using Ethereum Web3.js Library with Moonbeam

·

Introduction

Web3.js is a collection of libraries that allow developers to interact with Ethereum nodes using JavaScript via HTTP, IPC, or WebSocket protocols. Moonbeam offers Ethereum-compatible APIs that support Ethereum-style JSON-RPC calls, enabling developers to use the Web3.js library to interact with Moonbeam nodes just as they would with Ethereum.

This guide will walk you through how to use the Web3.js library to send transactions and deploy contracts on Moonbeam networks. The same instructions apply to Moonbeam, Moonriver, and Moonbeam development nodes.

Note that Web3.js has officially entered maintenance mode. For updated Ethereum library support, consider using Ethers.js or viem as alternatives.

Prerequisites

Before starting this tutorial, ensure you have the following:

The examples in this tutorial are based on Ubuntu 22.04 and macOS environments. Windows users may need to adjust the commands accordingly.

Start by creating a basic JavaScript project. First, create a directory to store all your files and initialize the project:

mkdir web3-examples && cd web3-examples && npm init -y

Install the Web3.js library and the Solidity compiler using npm:

npm install web3 [email protected]

Setting Up Web3.js with Moonbeam

You can configure Web3.js for any Moonbeam network. For Moonbeam or Moonriver networks, obtain your own endpoint from a supported network endpoint provider.

The simplest setup for each network is as follows:

Moonbeam Setup

const { Web3 } = require('web3');
const web3 = new Web3('INSERT_RPC_API_ENDPOINT');

Moonriver Setup

const { Web3 } = require('web3');
const web3 = new Web3('INSERT_RPC_API_ENDPOINT');

Moonbase Alpha Setup

const { Web3 } = require('web3');
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');

Moonbeam Dev Node Setup

const { Web3 } = require('web3');
const web3 = new Web3('http://127.0.0.1:9944');

Save these code snippets as you'll use them in the scripts in the following sections.

Sending a Transaction

In this section, you'll create scripts to check account balances before and after sending a transaction, and another script to execute the transaction itself.

Balance Check Script

Create a balances.js file:

touch balances.js

Add the following code to the file:

// 1. Add the Web3 provider logic here
const { Web3 } = require('web3');
const web3 = new Web3('INSERT_RPC_API_ENDPOINT');

// 2. Create address variables
const addressFrom = 'INSERT_FROM_ADDRESS';
const addressTo = 'INSERT_TO_ADDRESS';

// 3. Create balances function
const balances = async () => {
  // 4. Fetch balance info
  const balanceFrom = web3.utils.fromWei(
    await web3.eth.getBalance(addressFrom),
    'ether'
  );
  const balanceTo = web3.utils.fromWei(
    await web3.eth.getBalance(addressTo),
    'ether'
  );
  
  console.log(`The balance of ${addressFrom} is: ${balanceFrom} DEV`);
  console.log(`The balance of ${addressTo} is: ${balanceTo} DEV`);
};

// 5. Call balances function
balances();

Run the script to get account balances:

node balances.js

If successful, the balances of both addresses will be displayed in DEV.

Transaction Script

Create a transaction.js file:

touch transaction.js

Add the following code to execute a transaction:

// 1. Add the Web3 provider logic here
const { Web3 } = require('web3');
const web3 = new Web3('INSERT_RPC_API_ENDPOINT');

// 2. Create account variables
const accountFrom = {
  privateKey: 'INSERT_YOUR_PRIVATE_KEY',
  address: 'INSERT_PUBLIC_ADDRESS_OF_PK',
};
const addressTo = 'INSERT_TO_ADDRESS';

// 3. Create send function
const send = async () => {
  console.log(
    `Attempting to send transaction from ${accountFrom.address} to ${addressTo}`
  );

  // 4. Sign tx with PK
  const createTransaction = await web3.eth.accounts.signTransaction(
    {
      gas: 21000,
      to: addressTo,
      value: web3.utils.toWei('1', 'ether'),
      gasPrice: await web3.eth.getGasPrice(),
      nonce: await web3.eth.getTransactionCount(accountFrom.address),
    },
    accountFrom.privateKey
  );

  // 5. Send tx and wait for receipt
  const createReceipt = await web3.eth.sendSignedTransaction(
    createTransaction.rawTransaction
  );
  console.log(
    `Transaction successful with hash: ${createReceipt.transactionHash}`
  );
};

// 6. Call send function
send();

Run the script to execute the transaction:

node transaction.js

If the transaction is successful, the transaction hash will be displayed in the terminal. You can use the balances.js script again to verify the balance changes.

Deploying a Contract

Create a Sample Contract

Create a file named Incrementer.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Incrementer {
    uint256 public number;

    constructor(uint256 _initialNumber) {
        number = _initialNumber;
    }

    function increment(uint256 _value) public {
        number = number + _value;
    }

    function reset() public {
        number = 0;
    }
}

Compile Contract Script

Create a compile.js file:

touch compile.js

Add the following code to compile the contract:

// 1. Import packages
const fs = require('fs');
const solc = require('solc');

// 2. Get path and load contract
const source = fs.readFileSync('Incrementer.sol', 'utf8');

// 3. Create input object
const input = {
  language: 'Solidity',
  sources: {
    'Incrementer.sol': {
      content: source,
    },
  },
  settings: {
    outputSelection: {
      '*': {
        '*': ['*'],
      },
    },
  },
};

// 4. Compile the contract
const tempFile = JSON.parse(solc.compile(JSON.stringify(input)));
const contractFile = tempFile.contracts['Incrementer.sol']['Incrementer'];

// 5. Export contract data
module.exports = contractFile;

Deploy Contract Script

Create a deploy.js file:

touch deploy.js

Add the following code to deploy the contract:

// 1. Import the contract file
const contractFile = require('./compile');

// 2. Add the Web3 provider logic here
const { Web3 } = require('web3');
const web3 = new Web3('INSERT_RPC_API_ENDPOINT');

// 3. Create address variables
const accountFrom = {
  privateKey: 'INSERT_YOUR_PRIVATE_KEY',
  address: 'INSERT_PUBLIC_ADDRESS_OF_PK',
};

// 4. Get the bytecode and ABI
const bytecode = contractFile.evm.bytecode.object;
const abi = contractFile.abi;

// 5. Create deploy function
const deploy = async () => {
  console.log(`Attempting to deploy from account ${accountFrom.address}`);

  // 6. Create contract instance
  const incrementer = new web3.eth.Contract(abi);

  // 7. Create constructor tx
  const incrementerTx = incrementer.deploy({
    data: bytecode,
    arguments: [5],
  });

  // 8. Sign transaction and send
  const createTransaction = await web3.eth.accounts.signTransaction(
    {
      data: incrementerTx.encodeABI(),
      gas: await incrementerTx.estimateGas(),
      gasPrice: await web3.eth.getGasPrice(),
      nonce: await web3.eth.getTransactionCount(accountFrom.address),
    },
    accountFrom.privateKey
  );

  // 9. Send tx and wait for receipt
  const createReceipt = await web3.eth.sendSignedTransaction(
    createTransaction.rawTransaction
  );
  console.log(`Contract deployed at address: ${createReceipt.contractAddress}`);
};

// 10. Call deploy function
deploy();

Run the script to deploy the contract:

node deploy.js

If successful, the contract address will be displayed in the terminal.

Reading Contract Data (Call Function)

Create a get.js file:

touch get.js

Add the following code to read contract data:

// 1. Import the contract ABI
const { abi } = require('./compile');

// 2. Add the Web3 provider logic here
const { Web3 } = require('web3');
const web3 = new Web3('INSERT_RPC_API_ENDPOINT');

// 3. Create address variables
const contractAddress = 'INSERT_CONTRACT_ADDRESS';

// 4. Create contract instance
const incrementer = new web3.eth.Contract(abi, contractAddress);

// 5. Create get function
const get = async () => {
  console.log(`Making a call to contract at address: ${contractAddress}`);
  
  // 6. Call contract
  const data = await incrementer.methods.number().call();
  console.log(`The current number stored is: ${data}`);
};

// 7. Call get function
get();

Run the script to read the contract data:

node get.js

If successful, the current number value will be displayed.

Interacting with Contract (Send Function)

Create files for increment and reset functions:

touch increment.js reset.js

Add the following code to increment.js:

// 1. Import the contract ABI
const { abi } = require('./compile');

// 2. Add the Web3 provider logic here
const { Web3 } = require('web3');
const web3 = new Web3('INSERT_RPC_API_ENDPOINT');

// 3. Create variables
const accountFrom = {
  privateKey: 'INSERT_YOUR_PRIVATE_KEY',
  address: 'INSERT_PUBLIC_ADDRESS_OF_PK',
};
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
const _value = 3;

// 4. Create contract instance
const incrementer = new web3.eth.Contract(abi, contractAddress);

// 5. Build increment tx
const incrementTx = incrementer.methods.increment(_value);

// 6. Create increment function
const increment = async () => {
  console.log(
    `Calling the increment by ${_value} function in contract at address: ${contractAddress}`
  );

  // 7. Prepare and sign tx with PK
  const createTransaction = await web3.eth.accounts.signTransaction(
    {
      to: contractAddress,
      data: incrementTx.encodeABI(),
      gas: await incrementTx.estimateGas(),
      gasPrice: await web3.eth.getGasPrice(),
      nonce: await web3.eth.getTransactionCount(accountFrom.address),
    },
    accountFrom.privateKey
  );

  // 8. Send tx and wait for receipt
  const createReceipt = await web3.eth.sendSignedTransaction(
    createTransaction.rawTransaction
  );
  console.log(`Tx successful with hash: ${createReceipt.transactionHash}`);
};

// 9. Call increment function
increment();

Run the increment script:

node increment.js

Add the following code to reset.js:

// 1. Import the contract ABI
const { abi } = require('./compile');

// 2. Add the Web3 provider logic here
const { Web3 } = require('web3');
const web3 = new Web3('INSERT_RPC_API_ENDPOINT');

// 3. Create variables
const accountFrom = {
  privateKey: 'INSERT_YOUR_PRIVATE_KEY',
  address: 'INSERT_PUBLIC_ADDRESS_OF_PK',
};
const contractAddress = 'INSERT_CONTRACT_ADDRESS';

// 4. Create Contract Instance
const incrementer = new web3.eth.Contract(abi, contractAddress);

// 5. Build reset tx
const resetTx = incrementer.methods.reset();

// 6. Create reset function
const reset = async () => {
  console.log(
    `Calling the reset function in contract at address: ${contractAddress}`
  );

  // 7. Sign tx with PK
  const createTransaction = await web3.eth.accounts.signTransaction(
    {
      to: contractAddress,
      data: resetTx.encodeABI(),
      gas: await resetTx.estimateGas(),
      gasPrice: await web3.eth.getGasPrice(),
      nonce: await web3.eth.getTransactionCount(accountFrom.address),
    },
    accountFrom.privateKey
  );

  // 8. Send tx and wait for receipt
  const createReceipt = await web3.eth.sendSignedTransaction(
    createTransaction.rawTransaction
  );
  console.log(`Tx successful with hash: ${createReceipt.transactionHash}`);
};

// 9. Call reset function
reset();

Run the reset script:

node reset.js

Use the get.js script to verify the number value changes after each operation.

👉 Explore more blockchain development strategies

Frequently Asked Questions

What is Web3.js and why is it important for Ethereum development?

Web3.js is a collection of JavaScript libraries that allow developers to interact with Ethereum nodes using HTTP, IPC, or WebSocket protocols. It provides a convenient way to build decentralized applications (dApps) that can read blockchain data, send transactions, and interact with smart contracts. While newer libraries like Ethers.js and viem are now recommended for development, understanding Web3.js remains valuable for maintaining existing projects.

Can I use Web3.js with Moonbeam networks?

Yes, Moonbeam networks are fully compatible with Ethereum-style JSON-RPC calls, which means you can use Web3.js to interact with Moonbeam, Moonriver, and Moonbase Alpha just as you would with Ethereum. The same code and patterns work across these networks, making it easy for Ethereum developers to transition to Moonbeam ecosystems.

How do I estimate gas costs for transactions on Moonbeam?

You can estimate gas costs using the web3.eth.estimateGas() method, which returns the approximate amount of gas needed for a transaction. This helps in setting appropriate gas limits and avoiding failed transactions. Remember that gas prices on Moonbeam networks may differ from Ethereum mainnet, so it's important to check current network conditions.

What security precautions should I take when using Web3.js?

Never expose private keys in your code or version control systems. Use environment variables or secure key management systems to handle sensitive information. Additionally, always validate contract addresses and user inputs, and consider implementing transaction confirmations before executing critical operations. For production applications, use hardware wallets or dedicated signing services.

How can I handle errors and exceptions in Web3.js applications?

Web3.js methods return promises that can be handled with .catch() blocks or try/catch patterns in async functions. Common errors include insufficient funds, incorrect nonce values, and transaction timeouts. Implement proper error handling to provide user feedback and gracefully handle network issues or transaction failures.

Are there alternatives to Web3.js for Moonbeam development?

Yes, consider using Ethers.js or viem as modern alternatives to Web3.js. These libraries offer improved TypeScript support, better tree shaking capabilities, and more intuitive APIs. Moonbeam fully supports these libraries, and they may provide better performance and developer experience for new projects.