paint-brush
How to Deploy a Smart Contract to Ethereum Network Using dRPC API Key and Endpointby@ileolami
3,012 reads
3,012 reads

How to Deploy a Smart Contract to Ethereum Network Using dRPC API Key and Endpoint

by IleolamiSeptember 11th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

In this article, you will write, compile, test and deploy a coffee payment smart contract to Ethereum Sepolia Testnet using dRPC endpoint and API key. The features include: Payment for Coffee, Reviewing the price of Coffee, Retrieving Total number of Coffee sold and Total amount of money made.
featured image - How to Deploy a Smart Contract to Ethereum Network Using dRPC API Key and Endpoint
Ileolami HackerNoon profile picture

Introduction

From understanding the tech stack for Web3 DApp development, you must have learned the core tech stack for web3 dApp development, the role of RPC in dApp development, and how to use dRPC to create an account, generate an API key, endpoints, endpoints analytics, add funds to your dRPC Account, and check your balance.

The role of dRPC in deploying smart contracts is to simplify the process of setting up an Ethereum node, making it easier for developers to interact and deploy with just one line of code.

In this article, you will write, compile, test, and deploy a coffee payment smart contract to Ethereum Sepolia Testnet using dRPC endpoint and API key.

The features include:

  1. Payment for Coffee
  2. Retrieving the price of Coffee
  3. Retrieving Total number of Coffee sold and Total amount of money made

Let’s get your hands dirty.

Prerequisites

  1. Already fund your account with Sepolia Faucet.
  2. Have a Wallet e.g., Metamask.
  3. Code Editor.
  4. Already installed any Js libraries or frameworks of your choice (e.g React.js, Next.js etc).
  5. Jar of Water.

Technologies and Tools Needed

  1. Solidity.
  2. React.js using Vite.js(Typescript)
  3. Hardhat.
  4. Web3.js.
  5. Dotenv.
  6. dRPC API Key and Endpoint.
  7. Your account private key.
  8. MetaMask

Writing the Coffee Payment Smart Contract

  1. Create a Folder under your root directory, and name it contracts.
  2. Create a File under the contracts folder, and name it coffee.sol.


A file directory is shown with a folder named "contracts" and a file named "coffee.sol" inside the folder.


you will be using solidity to write the smart contract. Solidity files are named with the .sol extension because it is the standard file extension for Solidity source code.


  1. Add the following source code to the coffee.sol :



// SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0;

contract Coffee { 
uint256 public constant coffeePrice = 0.0002 ether; 
uint256 public totalCoffeesSold; 
uint256 public totalEtherReceived;

// Custom error definitions
error QuantityMustBeGreaterThanZero();
error InsufficientEtherSent(uint256 required, uint256 sent);
error DirectEtherTransferNotAllowed();

// Event to log coffee purchases
event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost);

// Function to buy coffee
function buyCoffee(uint256 quantity) external payable {
    if (quantity <= 0) {
        revert QuantityMustBeGreaterThanZero();
    }

    uint256 totalCost = coffeePrice * quantity;
    
    if (msg.value > totalCost) {
        revert InsufficientEtherSent(totalCost, msg.value);
    }

    // Update the total coffees sold and total ether received
    totalCoffeesSold += quantity;
    totalEtherReceived += totalCost;
    console.log("Total ether received updated:", totalEtherReceived);
    console.log("Total coffee sold updated:", totalCoffeesSold);
    // Emit the purchase event
    emit CoffeePurchased(msg.sender, quantity, totalCost);

    // Refund excess Ether sent
    if (msg.value > totalCost) {
        uint256 refundAmount = msg.value - totalCost;
        payable(msg.sender).transfer(refundAmount);
    }
}

// Fallback function to handle Ether sent directly to the contract
receive() external payable {
    revert DirectEtherTransferNotAllowed();
}

// Public view functions to get totals
function getTotalCoffeesSold() external view returns (uint256) {
    console.log("getTotalCoffeesSold :", totalCoffeesSold);
    return totalCoffeesSold;
}

function getTotalEtherReceived() external view returns (uint256) {
     console.log("getTotalEtherReceived :", totalEtherReceived);
            return totalEtherReceived;
}

}


Pragma

  • //SPDX-License-Identifier: MIT: This license identifier indicates that the code is licensed under the Massachusetts Institute of Technology (MIT) License.
  • pragma solidity >=0.8.0 <0.9.0;: Specifies that the code is written for Solidity versions between 0.8.0 (inclusive) and 0.9.0 (exclusive).

State Variable

uint256 public constant coffeePrice  = 0.0002 ether;
uint256 public totalCoffeesSold;
uint256 public totalEtherReceived;
  • coffeePrice: Set as a constant value of 0.0002 ether.
  • totalCoffeesSold: Tracks the number of coffees sold.
  • totalEtherReceived: Tracks the total Ether received by the contract.

Custom Errors

Custom errors in Solidity are error messages that are tailored to a specific use case, rather than the default error messages that are provided by the programming language. They can help improve the user experience, and can also help with debugging and maintaining smart contracts.

To define a custom error in Solidity, you can use the following syntax:

  • error: This keyword is used to define a custom error
  • Unique name: The error must have a unique name
  • Parameters: If you want to include specific details or parameters in the error message, you can add them in parentheses after the error name.
    error QuantityMustBeGreaterThanZero();  
    error InsufficientEtherSent(uint256 required, uint256 sent);  
    error DirectEtherTransferNotAllowed();  
  • QuantityMustBeGreaterThanZero(): Ensures the quantity is greater than zero.
  • InsufficientEtherSent(uint256 required, uint256 sent): Ensures the Ether sent is sufficient.
  • DirectEtherTransferNotAllowed(): Prevents direct Ether transfers to the contract.

Events

An event is a part of the contract that stores the arguments passed in the transaction logs when emitted. Events are usually used to inform the calling application about the contract's current state using the EVM's logging feature. They notify applications about changes made to the contracts, which can then be used to run related logic.

event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost);
  • CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost): Logs coffee purchases.

Functions

Functions are self-contained modules of code that accomplish a specific task. They eliminate the redundancy of rewriting the same piece of code. Instead, devs can call a function in the program when it’s necessary.

   
  function buyCoffee(uint256 quantity) external payable {
        if (quantity <= 0) {
            revert QuantityMustBeGreaterThanZero();
        }

        uint256 totalCost = coffeePrice * quantity;
        
        if (msg.value > totalCost) {
            revert InsufficientEtherSent(totalCost, msg.value);
        }

        // Update the total coffees sold and total ether received
        totalCoffeesSold += quantity;
        totalEtherReceived += totalCost;
        console.log("Total ether received updated:", totalEtherReceived);
        console.log("Total coffee sold updated:", totalCoffeesSold);
        // Emit the purchase event
        emit CoffeePurchased(msg.sender, quantity, totalCost);

        // Refund excess Ether sent
        if (msg.value > totalCost) {
            uint256 refundAmount = msg.value - totalCost;
            payable(msg.sender).transfer(refundAmount);
        }
    }

   receive() external payable {
        revert DirectEtherTransferNotAllowed();
    }


  function getTotalCoffeesSold() external view returns (uint256) {
        console.log("getTotalCoffeesSold :", totalCoffeesSold);
        return totalCoffeesSold;
    }

    function getTotalEtherReceived() external view returns (uint256) {
         console.log("getTotalEtherReceived :", totalEtherReceived);
                return totalEtherReceived;
    }

















  • buyCoffee(uint256 quantity) external payable: Handles coffee purchases and carries out the following operations:a. Check if the quantity is valid.b. Calculates the total cost.

c. Ensures sufficient Ether is sent.  
d. Updates state variables.  


e. Emits the purchase event.f. Refunds excess Ether.

  • receive() external payable: Reverts direct Ether transfers in case someone send funds to the contract address directly.
  • getTotalCoffeesSold() external view returns (uint256): Returns the total coffees sold.
  • getTotalEtherReceived() external view returns (uint256): Returns the total Ether received.

Compiling the Coffee Payment Smart Contract

Here, you will be using Hardhat to compile the smart contract.

  1. Install Hardhat using the following command prompt.

npm install --save-dev hardhat

You will get the response below after a successful installation.


A terminal displaying the output of installing hardhat..


  1. In the same directory where you initialize hardhat using this command prompt:

npx hardhat init


  1. Select Create a Javascript project by using the arrow down button and press enter.


  1. Press enter to install in the root folder
  2. Accept all the prompts using the y on your keyboard including the @nomicfoundation/hardhat-toolbox dependencies
  3. You see this response below showcasing that you've successfully initialize

You will notce some new folders and files have been added to your project. e.g., Lock.sol, iginition/modules, test/Lock.js and hardhat.config.cjs. Don't worry about them.

The only useful one are the iginition/modules and hardhat.config.cjs. You will know what they are used for later on. Feel free to delete Lock.sol under contracts folder and Lock.js under iginition/modules folder.


  1. Compile the contract using the following command prompt:

npx hardhat compile

  1. You see additional folders and files like this.

Inside the Coffee.json file is the ABI code in JSON format which you will call when interacting with the smart contract.

{
  "_format": "hh-sol-artifact-1",
  "contractName": "Coffee",
  "sourceName": "contracts/coffee.sol",
  "abi": [
    {
      "inputs": [],
      "name": "DirectEtherTransferNotAllowed",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "required",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "sent",
          "type": "uint256"
        }
      ],
      "name": "InsufficientEtherSent",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "QuantityMustBeGreaterThanZero",
      "type": "error"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "buyer",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "quantity",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "totalCost",
          "type": "uint256"
        }
      ],
      "name": "CoffeePurchased",
      "type": "event"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "quantity",
          "type": "uint256"
        }
      ],
      "name": "buyCoffee",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "coffeePrice",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getTotalCoffeesSold",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getTotalEtherReceived",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "totalCoffeesSold",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "totalEtherReceived",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "stateMutability": "payable",
      "type": "receive"
    }
  ],
  "bytecode": "0x608060405234801561001057600080fd5b506107d7806100206000396000f3fe6080604052600436106100595760003560e01c80631c8a403814610094578063657b2d89146100bf5780637ef3e741146100ea5780639fd66f9014610115578063b03b4a2914610140578063e926b8d01461015c5761008f565b3661008f576040517ebbbfa300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600080fd5b3480156100a057600080fd5b506100a9610187565b6040516100b69190610537565b60405180910390f35b3480156100cb57600080fd5b506100d4610191565b6040516100e19190610537565b60405180910390f35b3480156100f657600080fd5b506100ff6101dc565b60405161010c9190610537565b60405180910390f35b34801561012157600080fd5b5061012a6101e2565b6040516101379190610537565b60405180910390f35b61015a60048036038101906101559190610583565b6101e8565b005b34801561016857600080fd5b506101716103e7565b60405161017e9190610537565b60405180910390f35b65b5e620f4800081565b60006101d46040518060400160405280601781526020017f676574546f74616c45746865725265636569766564203a000000000000000000815250600154610432565b600154905090565b60015481565b60005481565b60008111610222576040517f0cdcd02000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008165b5e620f4800061023691906105df565b90508034111561027f5780346040517f8ad0844f000000000000000000000000000000000000000000000000000000008152600401610276929190610621565b60405180910390fd5b81600080828254610290919061064a565b9250508190555080600160008282546102a9919061064a565b925050819055506102f16040518060400160405280601d81526020017f546f74616c20657468657220726563656976656420757064617465643a000000815250600154610432565b6103326040518060400160405280601a81526020017f546f74616c20636f6666656520736f6c6420757064617465643a000000000000815250600054610432565b3373ffffffffffffffffffffffffffffffffffffffff167fb706f7a46856e7a0e4f8f504c23f2ac26950209db23c2125108eed5ef9e333d3838360405161037a929190610621565b60405180910390a2803411156103e35760008134610398919061067e565b90503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501580156103e0573d6000803e3d6000fd5b50505b5050565b600061042a6040518060400160405280601581526020017f676574546f74616c436f6666656573536f6c64203a0000000000000000000000815250600054610432565b600054905090565b6104ca8282604051602401610448929190610742565b6040516020818303038152906040527fb60e72cc000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506104ce565b5050565b6104e5816104dd6104e8610509565b63ffffffff16565b50565b60006a636f6e736f6c652e6c6f679050600080835160208501845afa505050565b610514819050919050565b61051c610772565b565b6000819050919050565b6105318161051e565b82525050565b600060208201905061054c6000830184610528565b92915050565b600080fd5b6105608161051e565b811461056b57600080fd5b50565b60008135905061057d81610557565b92915050565b60006020828403121561059957610598610552565b5b60006105a78482850161056e565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006105ea8261051e565b91506105f58361051e565b92508282026106038161051e565b9150828204841483151761061a576106196105b0565b5b5092915050565b60006040820190506106366000830185610528565b6106436020830184610528565b9392505050565b60006106558261051e565b91506106608361051e565b9250828201905080821115610678576106776105b0565b5b92915050565b60006106898261051e565b91506106948361051e565b92508282039050818111156106ac576106ab6105b0565b5b92915050565b600081519050919050565b600082825260208201905092915050565b60005b838110156106ec5780820151818401526020810190506106d1565b60008484015250505050565b6000601f19601f8301169050919050565b6000610714826106b2565b61071e81856106bd565b935061072e8185602086016106ce565b610737816106f8565b840191505092915050565b6000604082019050818103600083015261075c8185610709565b905061076b6020830184610528565b9392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052605160045260246000fdfea264697066735822122006c7d91368c8390cb4f21f6314ccd362b6d56cb17994af7009b53e7fb92411a864736f6c63430008180033",
  "deployedBytecode": "0x6080604052600436106100595760003560e01c80631c8a403814610094578063657b2d89146100bf5780637ef3e741146100ea5780639fd66f9014610115578063b03b4a2914610140578063e926b8d01461015c5761008f565b3661008f576040517ebbbfa300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600080fd5b3480156100a057600080fd5b506100a9610187565b6040516100b69190610537565b60405180910390f35b3480156100cb57600080fd5b506100d4610191565b6040516100e19190610537565b60405180910390f35b3480156100f657600080fd5b506100ff6101dc565b60405161010c9190610537565b60405180910390f35b34801561012157600080fd5b5061012a6101e2565b6040516101379190610537565b60405180910390f35b61015a60048036038101906101559190610583565b6101e8565b005b34801561016857600080fd5b506101716103e7565b60405161017e9190610537565b60405180910390f35b65b5e620f4800081565b60006101d46040518060400160405280601781526020017f676574546f74616c45746865725265636569766564203a000000000000000000815250600154610432565b600154905090565b60015481565b60005481565b60008111610222576040517f0cdcd02000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008165b5e620f4800061023691906105df565b90508034111561027f5780346040517f8ad0844f000000000000000000000000000000000000000000000000000000008152600401610276929190610621565b60405180910390fd5b81600080828254610290919061064a565b9250508190555080600160008282546102a9919061064a565b925050819055506102f16040518060400160405280601d81526020017f546f74616c20657468657220726563656976656420757064617465643a000000815250600154610432565b6103326040518060400160405280601a81526020017f546f74616c20636f6666656520736f6c6420757064617465643a000000000000815250600054610432565b3373ffffffffffffffffffffffffffffffffffffffff167fb706f7a46856e7a0e4f8f504c23f2ac26950209db23c2125108eed5ef9e333d3838360405161037a929190610621565b60405180910390a2803411156103e35760008134610398919061067e565b90503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501580156103e0573d6000803e3d6000fd5b50505b5050565b600061042a6040518060400160405280601581526020017f676574546f74616c436f6666656573536f6c64203a0000000000000000000000815250600054610432565b600054905090565b6104ca8282604051602401610448929190610742565b6040516020818303038152906040527fb60e72cc000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506104ce565b5050565b6104e5816104dd6104e8610509565b63ffffffff16565b50565b60006a636f6e736f6c652e6c6f679050600080835160208501845afa505050565b610514819050919050565b61051c610772565b565b6000819050919050565b6105318161051e565b82525050565b600060208201905061054c6000830184610528565b92915050565b600080fd5b6105608161051e565b811461056b57600080fd5b50565b60008135905061057d81610557565b92915050565b60006020828403121561059957610598610552565b5b60006105a78482850161056e565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006105ea8261051e565b91506105f58361051e565b92508282026106038161051e565b9150828204841483151761061a576106196105b0565b5b5092915050565b60006040820190506106366000830185610528565b6106436020830184610528565b9392505050565b60006106558261051e565b91506106608361051e565b9250828201905080821115610678576106776105b0565b5b92915050565b60006106898261051e565b91506106948361051e565b92508282039050818111156106ac576106ab6105b0565b5b92915050565b600081519050919050565b600082825260208201905092915050565b60005b838110156106ec5780820151818401526020810190506106d1565b60008484015250505050565b6000601f19601f8301169050919050565b6000610714826106b2565b61071e81856106bd565b935061072e8185602086016106ce565b610737816106f8565b840191505092915050565b6000604082019050818103600083015261075c8185610709565b905061076b6020830184610528565b9392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052605160045260246000fdfea264697066735822122006c7d91368c8390cb4f21f6314ccd362b6d56cb17994af7009b53e7fb92411a864736f6c63430008180033",
  "linkReferences": {},
  "deployedLinkReferences": {}
}

Testing the Smart Contract

Writing an automated test script while building your smart contract is crucial and highly recommended. It acts like a two-factor authentication (2FA), ensuring that your smart contract performs as expected before deploying it to the live network.


Under the test folder create a new file, and name it Coffee.cjs. Inside the file, paste this code below:


const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers.js");
const { expect } = require("chai");
const pkg = require("hardhat");
const ABI = require('../artifacts/contracts/coffee.sol/Coffee.json');
const { web3 } = pkg;

describe("Coffee Contract", function () {
  // Fixture to deploy the Coffee contract
  async function deployCoffeeFixture() {
    const coffeeContract = new web3.eth.Contract(ABI.abi);
    coffeeContract.handleRevert = true;

    const [deployer, buyer] = await web3.eth.getAccounts();
    const rawContract = coffeeContract.deploy({
      data: ABI.bytecode,
    });

    // Estimate gas for the deployment
    const estimateGas = await rawContract.estimateGas({ from: deployer });

    // Deploy the contract
    const coffee = await rawContract.send({
      from: deployer,
      gas: estimateGas.toString(),
      gasPrice: "10000000000",
    });

    console.log("Coffee contract deployed to: ", coffee.options.address);
    return { coffee, deployer, buyer, rawContract };
  }

  describe("Deployment", function () {
    // Test to check initial values after deployment
    it("Should set the initial values correctly", async function () {
      const { coffee } = await loadFixture(deployCoffeeFixture);
      const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();
      const totalEtherReceived = await coffee.methods.totalEtherReceived().call();

      expect(totalCoffeesSold).to.equal("0");
      expect(totalEtherReceived).to.equal("0");
    });
  });

  describe("Buying Coffee", function () {
    // Test to check coffee purchase and event emission
    it("Should purchase coffee and emit an event", async function () {
      const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
      const quantity = 3;
      const totalCost = web3.utils.toWei("0.0006", "ether");

      // Buyer purchases coffee
      const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });

      // Check event
      const event = receipt.events.CoffeePurchased;
      expect(event).to.exist;
      expect(event.returnValues.buyer).to.equal(buyer);
      expect(event.returnValues.quantity).to.equal(String(quantity));
      expect(event.returnValues.totalCost).to.equal(totalCost);
    });

    // Test to check revert when quantity is zero
    it("Should revert if the quantity is zero", async function () {
            const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
            expect(
              coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") })
            ).to.be.revertedWith("QuantityMustBeGreaterThanZero");
          });

          
    // Test to check if totalCoffeesSold and totalEtherReceived are updated correctly
    it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () {
      const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
      const quantity = 5;
      const totalCost = web3.utils.toWei("0.001", "ether");

      await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });

      const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();
      const totalEtherReceived = await coffee.methods.totalEtherReceived().call();

      expect(totalCoffeesSold).to.equal(String(quantity));
      expect(totalEtherReceived).to.equal(totalCost);
    });

   
  });

  describe("Fallback function", function () {
    // Test to check revert when ether is sent directly to the contract
    it("Should revert if ether is sent directly to the contract", async function () {
      const { coffee, buyer } = await loadFixture(deployCoffeeFixture);

      expect(
        web3.eth.sendTransaction({
          from: buyer,
          to: coffee.options.address,
          value: web3.utils.toWei("0.001", "ether"),
        })
      ).to.be.revertedWith("DirectEtherTransferNotAllowed");
    });
  });
});


This code tests the functionality of the Coffee smart contract. It includes tests for deployment, buying coffee, and handling direct Ether transfers to the contract.

Here is a breakdown:

Fixture Function:

 deployCoffeeFixture

async function deployCoffeeFixture() {

const coffeeContract = new web3.eth.Contract(ABI.abi);

coffeeContract.handleRevert = true;

const [deployer, buyer] = await web3.eth.getAccounts();

const rawContract = coffeeContract.deploy({

data: ABI.bytecode,

});

const estimateGas = await rawContract.estimateGas({ from: deployer });

const coffee = await rawContract.send({

from: deployer,

gas: estimateGas.toString(),

gasPrice: "10000000000",

});

console.log("Coffee contract deployed to: ", coffee.options.address);

return { coffee, deployer, buyer, rawContract };

}
  • Deploys the Coffee contract: Creates a new contract instance and deploys it using the deployer's account.
  • Estimates gas: Estimates the gas required for deployment.
  • Returns: The deployed contract instance, deployer, and buyer accounts.

Deployment Tests

describe("Deployment", function () {
  it("Should set the initial values correctly", async function () {
    const { coffee } = await loadFixture(deployCoffeeFixture);
    const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();
    const totalEtherReceived = await coffee.methods.totalEtherReceived().call();
    expect(totalCoffeesSold).to.equal("0");
    expect(totalEtherReceived).to.equal("0");
  });
});
  • Checks initial values: Ensures that totalCoffeesSold and totalEtherReceived are set to zero after deployment.

Buying Coffee Tests

describe("Buying Coffee", function () {
  it("Should purchase coffee and emit an event", async function () {
    const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
    const quantity = 3;
    const totalCost = web3.utils.toWei("0.0006", "ether");
    const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });
    const event = receipt.events.CoffeePurchased;
    expect(event).to.exist;
    expect(event.returnValues.buyer).to.equal(buyer);
    expect(event.returnValues.quantity).to.equal(String(quantity));
    expect(event.returnValues.totalCost).to.equal(totalCost);
  });
  it("Should revert if the quantity is zero", async function () {
    const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
    expect(
      coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") })
    ).to.be.revertedWith("QuantityMustBeGreaterThanZero");
  });
  it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () {
    const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
    const quantity = 5;
    const totalCost = web3.utils.toWei("0.001", "ether");
    await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });
    const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();
    const totalEtherReceived = await coffee.methods.totalEtherReceived().call();
    expect(totalCoffeesSold).to.equal(String(quantity));
    expect(totalEtherReceived).to.equal(totalCost);

  });

});
  • Purchasing coffee and emitting an event: Tests that buying coffee updates the state and emits the CoffeePurchased event.
  • Reverting on zero quantity: Ensures the transaction reverts if the quantity is zero.
  • Updating state correctly: Verifies that totalCoffeesSold and totalEtherReceived are updated correctly after a purchase.

Fallback Function Test

describe("Fallback function", function () {
  it("Should revert if ether is sent directly to the contract", async function () {
    const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
    expect(
      web3.eth.sendTransaction({
        from: buyer,
        to: coffee.options.address,
        value: web3.utils.toWei("0.001", "ether"),
      })
    ).to.be.revertedWith("DirectEtherTransferNotAllowed");
  });
});
  • Reverting on direct Ether transfer: Ensures that sending Ether directly to the contract (without calling a function) reverts the transaction.

Testing the Smart Contract

After you have written the test script, you will:

  1. When running your contracts and tests on Hardhat Network, you can print logging messages and contract variables calling console.log() from your Solidity code. To use it, you have to import hardhat/console.sol in your contract code like this:
//SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0;

import "hardhat/console.sol";

contract Coffee {

//...

}


To test the contract, run the following command in your terminal:

npx hardhat test

You should have an output like this below: This shows that your smart contract functions the way it is expected.

If you run npx hardhat test it automatically compile and test the smart contract. You can try it out and let me know in the comment section.

Deploying the Smart Contract

Here, you will be deploying your smart contract to Sepolia Testnet. Testnet allows you to test your smart contract in an environment that mimics the Ethereum mainnet without incurring significant costs. If you are good with the function of the dApp, you can then redeploy to Ethereum Mainnet.

  1. Install the dotenv package and these dependencies.
npm install dotenv

npm install --save-dev @nomicfoundation/hardhat-web3-v4 'web3@4'

This will add Web3.Js and Dotenv to your project by including it in the 'node_modules' folder.


  1. import them into your hardhat.config.cjs file
require('dotenv').config();
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-web3-v4");
const HardhatUserConfig = require("hardhat/config");

module.exports = {
  solidity: "0.8.24",
  }
};
  1. Create an .env file in your root folder.
  2. Get your account private key from your MetaMask wallet and dRPC API key.
  3. Store them in your .env file.
DRPC_API_KEY=your_drpc_api_key 
PRIVATE_KEY=your_wallet_private_key
  1. Update the hardhat.config.cjs file to include the Sepolia Testnet Configuration:
require('dotenv').config();
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-web3-v4");
const HardhatUserConfig = require("hardhat/config");

const dRPC_API_KEY = process.env.VITE_dRPC_API_KEY;
const PRIVATE_KEY = process.env.VITE_PRIVATE_KEY;

module.exports = {
  solidity: "0.8.24",
  networks: {
    sepolia: {
      url: `https://lb.drpc.org/ogrpc?network=sepolia&dkey=${dRPC_API_KEY}`,
      accounts: [`0x${PRIVATE_KEY}`],
    }
  }
};
  1. Create a new script file under the ignition/module folder, and name it deploy.cjs. Add the following code to deploy your smart contract:
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

const CoffeeModule = buildModule("CoffeeModule", (m) => {
  const coffee = m.contract("Coffee");

  return { coffee };
});

module.exports = CoffeeModule;
  1. Deploy the smart contract by running the following command in your terminal:

npx hardhat ignition deploy ./ignition/modules/deploy.cjs --network sepolia


After running the command prompt, you will be asked to Confirm deploy to network sepolia (11155111)? (y/n), type in y . you should see the address of your deployed smart contract in the terminal upon successful deployment.

you can also access the contract address in the deployed_addresses.json file.


Screenshot of a Vite-Project file explorer and a deployed_addresses.json file opened.

Congratulations, you have successfully deployed your smart contract to Sepolia Testnet. 🎉

Conclusion

This article has taught you how to write payment smart contract, test, compile and deploy smart contract using the hardhat CLI.

In the next article, you will learn to build the front end for this dApp. This UI will consist of:

  1. Input field for the number of Coffees bought.
  2. A button that triggers a payment transaction and deducts it from your account.
  3. Display Total coffee bought and the amount received in ether and USD
  4. The price of Coffee itself both in ether and USD.

Reference

Beyond Default Messages: Mastering Custom Errors in Solidity

Mastering Custom Errors in Solidity: Elevate Your Smart Contracts Beyond Default Messages

What are Solidity Functions?

What are Events in Solidity?

Previous Article Next Article →