DEV Community

Cover image for How to Create Smart Contracts with Web3
Ancel Almeida
Ancel Almeida

Posted on

How to Create Smart Contracts with Web3

Smart contracts are self-executing agreements that run on the blockchain and enable decentralized applications (dapps) in the web3 ecosystem. Web3 is a term that refers to the next generation of the internet, where users have more control over their data, identity, and assets, and can interact directly with each other without intermediaries.

Photo by [Jonathan Borba](https://unsplash.com/@jonathanborba?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)Photo by Jonathan Borba on Unsplash

We will explore how to create smart contracts with web3 using JavaScript and Solidity, which are the most popular languages for web3 development. We will also use some tools and platforms that simplify the process of writing, deploying, and interacting with smart contracts.

What You Need

To get started, you will need the following:

  • Visual Studio Code: A code editor that supports syntax highlighting, debugging, and extensions for web3 development.

  • Ganache: A local blockchain simulator that allows you to test your smart contracts without spending real ether.

  • Node.js: A runtime environment that lets you run JavaScript code outside of a browser.

  • Web3.js: A library that provides an interface to interact with Ethereum nodes and smart contracts.

  • Solidity: A programming language designed for writing smart contracts on Ethereum and other blockchains.

You can install these tools by following the instructions on their respective websites or using a package manager like npm.

Writing a Smart Contract

A smart contract is a piece of code that defines the logic and rules of an agreement between two or more parties. For example, a smart contract can represent a purchase order, an insurance policy, or a game.

To write a smart contract, we will use Solidity, which is similar to JavaScript but has some specific features and syntax for blockchain development. Solidity files have a .sol extension and start with a pragma statement that specifies the compiler version.

Let's write a simple smart contract that represents a purchase order between a buyer and a seller. The contract will have the following features:

  • It will store the buyer's address, the seller's address, the item name, and the price.

  • It will have a constructor function that initializes these values when deploying the contract.

  • It will have a buy function that allows the buyer to pay for the item using ether.

  • It will have an event that emits when the buy function is executed.

The code for this contract is as follows:

// SPDX-License-Identifier: GPL-3.0
// Specify compiler version
pragma solidity ^0.8.0;
// Define contract name
contract PurchaseOrder {
  // Declare state variables
  address public buyer;
  address public seller;
  string public item;
  uint256 public price;
  // Declare event
  event Purchased(address buyer, address seller, string item);
  // Define constructor function
  constructor(address _buyer, address _seller,
              string memory _item,
              uint256 _price) {
    // Assign values to state variables
    buyer = _buyer;
    seller = _seller;
    item = _item;
    price = _price;
  }
  // Define buy function
  function buy() public payable {
    // Check if sender is buyer
    require(msg.sender == buyer,
            "Only buyer can buy");
    // Check if value is equal to price
    require(msg.value == price,
            "Value must be equal to price");
    // Transfer value to seller
    payable(seller).transfer(msg.value);
    // Emit event
    emit Purchased(buyer,seller,item);
  }
}
Enter fullscreen mode Exit fullscreen mode

Deploying a Smart Contract

To deploy a smart contract on Ethereum or any other blockchain platform compatible with web3.js (such as Moralis), we need two things:

  • An ABI (Application Binary Interface): A JSON file that describes how to interact with the contract's functions and events.

  • A bytecode: A hexadecimal string that represents the compiled version of the contract's code.

We can generate these files by using an online tool like Remix IDE or by using solc-js, which is a JavaScript wrapper for the Solidity compiler.

For simplicity, we will use Remix IDE in this tutorial. To do so,

  1. Open Remix IDE in your browser and create a new file named PurchaseOrder.sol.

  2. Copy-paste our PurchaseOrder.sol code into it.

  3. Click on the Solidity Compiler tab on the left panel and select the compiler version as ^0.8.0.

  4. Click on Compile PurchaseOrder.sol button.

  5. Click on the Compilation Details button below it.

  6. Copy-paste ABI JSON into another file named PurchaseOrderABI.json.

  7. Copy-paste the bytecode hex string into another file named PurchaseOrderBytecode.txt.

Now we have our ABI and bytecode ready for deployment.

Next,

  1. Open Ganache in your browser and click on the Quickstart Workspace button. This will connect Remix IDE to Ganache and show you 10 accounts with 100 ETH each.

  2. Click on the Deploy & Run Transactions tab on the left panel and select Web3 Provider as Environment.

  3. Click on the Deploy button below our PurchaseOrder contract name. This will deploy our contract to Ganache and show us its address, ABI, and functions.

  4. Copy-paste the contract address into another file named PurchaseOrderAddress.txt.

Now we have deployed our smart contract on Ganache using Remix IDE.

Interacting with a Smart Contract

To interact with a smart contract using web3.js, we need to do the following steps:

  • Create a web3 instance that connects to Ganache via HTTP provider.

  • Create a contract instance that uses our ABI and address.

  • Call or send transactions to our contract functions using our contract instance.

Let's write a JavaScript file that does these steps and prints out some information about our purchase order.

Create a new file named PurchaseOrder.js and copy-paste the following code into it:

// Import web3 library
const Web3 = require("web3");
// Import fs library for reading files
const fs = require("fs");
// Read ABI JSON file
const abi = JSON.parse(fs.readFileSync("PurchaseOrderABI.json"));
// Read bytecode hex string file
const bytecode = fs.readFileSync("PurchaseOrderBytecode.txt", "utf8");
// Read contract address file
const address = fs.readFileSync("PurchaseOrderAddress.txt", "utf8");
// Create web3 instance and connect to Ganache
const web3 = new Web3("http://localhost:8545");
// Create contract instance using ABI and address
const contract = new web3.eth.Contract(abi, address);
// Get buyer's address from Ganache (first account)
web3.eth.getAccounts().then(accounts => {
  const buyer = accounts[0];
// Get seller's address from Ganache (second account)
  const seller = accounts[1];
// Get item name and price from constructor arguments
  const item = web3.utils.hexToUtf8(bytecode.slice(-128, -64));
  const price = web3.utils.hexToNumber(bytecode.slice(-64));
// Print purchase order details
  console.log("Purchase Order Details:");
  console.log("Buyer: " + buyer);
  console.log("Seller: " + seller);
  console.log("Item: " + item);
  console.log("Price: " + price + " wei");
// Call buy function using buyer's account and value equal to price
  contract.methods.buy().send({from: buyer, value: price})
    .then(receipt => {
      // Print transaction receipt
      console.log("Transaction Receipt:");
      console.log(receipt);
// Get purchased event from receipt logs
      const event = receipt.events.Purchased;
// Print event details
      console.log("Event Details:");
      console.log(event.returnValues);
    })
    .catch(error => {
      // Print error message
      console.error(error.message);
    });
});
Enter fullscreen mode Exit fullscreen mode

This code will do the following:

  • It will import web3 library and fs library for reading files.

  • It will read our ABI JSON file, bytecode hex string file, and contract address file.

  • It will create a web3 instance that connects to Ganache via HTTP provider.

  • It will create a contract instance using our ABI and address.

  • It will get the buyer's address, seller's address, item name, and price from Ganache accounts and constructor arguments respectively.

  • It will print out these details on the console.

  • It will call the buy function of our contract using the buyer's account and value equal to price as parameters.

  • It will print out the transaction receipt on the console, which includes an event log of the Purchased event that emits when the buy function is executed.

  • It will print out the event details on the console, which include the buyer's address, seller's address, and item name as return values.

To run this code,

  1. Open a terminal window in your project folder where you have saved all your files (PurchaseOrder.sol, PurchaseOrderABI.json, PurchaseOrderBytecode.txt, PurchaseOrderAddress.txt, PurchaseOrder.js ).

  2. Make sure Ganache is running in another terminal window or browser tab.

  3. Type node PurchaseOrder.js in your terminal window and press enter.

You should see something like this:

Listening to Smart Contract Events

A smart contract can emit events that notify the listeners about some changes or actions that occurred on the contract. For example, our PurchaseOrder contract emits a Purchased event when the buy function is executed.

To listen to smart contract events using web3.js, we can use one of the following methods:

  • contract.once(event [, options], callback): This method will fetch an event only once and return undefined. It is useful for listening to events that occur only once, such as a constructor event.

  • contract.events.MyEvent([options] [, callback]): This method will fetch events named "MyEvent" and return an eventEmitter object. It is useful for listening to specific events that occur multiple times, such as a Transfer event.

  • contract.events.allEvents([options] [, callback]): This method will fetch all events emitted by the contract and return an eventEmitter object. It is useful for listening to all events that occur on the contract, regardless of their names.

  • contract.getPastEvents(event [, options] [, callback]): This method will fetch all past events available in the block range and return an array of event objects. It is useful for getting historical data or analyzing past transactions.

All these methods have similar parameters:

  • event: A string that specifies the name of the event to listen to or get. Use "*" for allEvents or getPastEvents methods.

  • options: An object that specifies some filters or settings for getting or listening to events, such as:

  • filter: An object that lets you filter events by indexed parameters, such as {from: "0x123…"} or {value: web3.utils.toWei("1", "ether")}.

  • fromBlock: A number or string that specifies the block number from which to get or listen to events. Use "latest" for the current block, "pending" for pending transactions, or "earliest" for the genesis block.

  • toBlock: A number or string that specifies the block number up to which to get past events. Use "latest" for the current block, "pending" for pending transactions, or "earliest" for the genesis block.

  • callback: A function that takes two arguments: error and result. The error argument will be null if no error occurred, and the result will be either undefined (for once method), an eventEmitter object (for events methods), or an array of event objects (for the getPastEvents method).

The event objects returned by these methods have some common properties:

  • event: A string that indicates the name of the event.

  • address: A string that indicates the address of the contract that emitted the event.

  • transactionHash: A string that indicates the hash of the transaction that triggered the event.

  • blockHash: A string that indicates the block number in the blockchain

  • blockNumber: A number that indicates the block number where the event was emitted.

  • logIndex: A number that indicates the position of the event within the block.

  • returnValues: An object that contains the values of the non-indexed parameters of the event.

The eventEmitter object returned by events methods has some common methods:

  • on(type, listener): This method registers a listener function for a given type of event, such as "data", "error", "changed", or "connected".

  • once(type, listener): This method registers a listener function for a given type of event, but only executes it once and then removes it.

  • off(type, listener): This method removes a listener function for a given type of event.

  • remove all listeners ([type]): This method removes all listeners or only those of the specified type.

Let's write some code that uses these methods to listen to our Purchased event.

Create a new file named PurchaseOrderEvents.js and copy-paste the following code into it:

// Import web3 library
const Web3 = require("web3");
// Import fs library for reading files
const fs = require("fs");
// Read ABI JSON file
const abi = JSON.parse(fs.readFileSync("PurchaseOrderABI.json"));
// Read contract address file
const address = fs.readFileSync("PurchaseOrderAddress.txt", "utf8");
// Create web3 instance and connect to Ganache via WebSocket provider
const web3 = new Web3("ws://localhost:8545");
// Create contract instance using ABI and address
const contract = new web3.eth.Contract(abi, address);
// Define a callback function for Purchased event
const purchasedCallback = (error, result) => {
  if (error) {
    // Print error message
    console.error(error.message);
  } else {
    // Print event details
    console.log("Purchased Event:");
    console.log(result.returnValues);
  }
};
// Listen to Purchased event using events method and callback function
contract.events.Purchased({}, purchasedCallback);
// Listen to Purchased event using once method and callback function
contract.once("Purchased", {}, purchasedCallback);
// Listen to all events using allEvents method and callback function
contract.events.allEvents({}, purchasedCallback);
// Get past Purchased events using getPastEvents method and callback function
contract.getPastEvents("Purchased", {}, purchasedCallback);
Enter fullscreen mode Exit fullscreen mode

This code will do the following:

  • It will import the web3 library and fs library for reading files.

  • It will read our ABI JSON file and contract address file.

  • It will create a web3 instance that connects to Ganache via the WebSocket provider. Note that we use ws:// instead of https:// protocol here, as WebSocket is required for subscribing to events.

  • It will create a contract instance using our ABI and address.

  • It will define a callback function that takes error and result arguments. The error argument will be null if no error occurred, and the result will be an event object. The callback function will print out either an error message or an event detail on the console.

  • It will use different methods to listen to our Purchased event or get past events. Each method takes an empty options object (as we don't need any filters) and our callback function as parameters.

To run this code,

  1. Open a terminal window in your project folder where you have saved all your files (PurchaseOrder.sol, PurchaseOrderABI.json, PurchaseOrderBytecode.txt, PurchaseOrderAddress.txt, PurchaseOrder.js, PurchaseOrderEvents.js ).

  2. Make sure Ganache is running in another terminal window or browser tab with WebSocket enabled (use -w flag).

  3. Type node PurchaseOrderEvents.js in your terminal window and press enter.

You should see something like this:

As you can see, we have successfully listened to our Purchased event using different methods. Note that each method has its own advantages and disadvantages depending on your use case. For example,

  • The events method is more efficient and real-time than the getPastEvents method, as it uses WebSocket instead of HTTP protocol and does not need to poll for new blocks.

  • The once method is more convenient than the events method for one-time events, as it automatically removes the listener after execution and does not need to be manually unsubscribed.

  • The allEvents method is more comprehensive than the events method for multiple events, as it listens to all possible events emitted by the contract and does not need to specify each event name.

  • The getPastEvents method is more reliable than the events method for historical data, as it can retrieve past events that may have been missed by WebSocket connection issues or node synchronization problems.

Conclusion

In this article, we have learned how to create smart contracts with web3 using JavaScript and Solidity. We have also learned how to deploy, interact, and listen to smart contract events using the web3.js library and Ganache simulator. We have seen some of the advantages and disadvantages of web3 and its associated technologies, such as blockchain, crypto, DeFi, and AI.

Web3 is an exciting and promising vision for the future of the internet, where users can have more control over their data, identity, and assets, and can interact directly with each other without intermediaries. However, web3 also poses some challenges and risks that need to be addressed carefully. Web3 is still in its early stages of development and adoption, so there is a lot of room for improvement and innovation.

Sample project code

Top comments (0)