DEV Community

Cover image for Developing a Rental DApp on CrossFi Using Hardhat
Azeez Abidoye
Azeez Abidoye

Posted on

Developing a Rental DApp on CrossFi Using Hardhat

What's CrossFi?

CrossFi is a blockchain platform built to enable seamless cross-chain asset transfers and decentralized finance (DeFi) applications. With a focus on interoperability, security, and scalability, CrossFi provides developers and users with a powerful ecosystem for deploying decentralized applications (DApps) that can operate across multiple blockchain networks.

Prerequisites

  • NodeJs
  • Metamask
  • Testnet ethers
  • Alchemy API URL

Dev Tools

npm install -g yarn
Enter fullscreen mode Exit fullscreen mode

Step 1: Creating and launching a new React project

npm create vite@latest rental-dapp --template react && cd rental-dapp
Enter fullscreen mode Exit fullscreen mode

✍️ Choose React and Javascript from the prompt to set up your project.

Step 2: Installing Hardhat Plugin for development

yarn add hardhat
Enter fullscreen mode Exit fullscreen mode

Step 3: Initializing Hardhat Ethereum development environment

npx hardhat init
Enter fullscreen mode Exit fullscreen mode

✍️ To set up your project, select Create a JavaScript project and proceed with the prompt.

Step 4: Configuring Hardhat for development

  • Open the hardhat.config.cjs file to set up the networks property
require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.28",
  networks: {
    crossfiTestnet: {
      chainId: 4157,
      url: "https://crossfi-testnet.g.alchemy.com/v2/wAz9j4RJUgEBiaMljD1yGbi45YBRKXTK",
      accounts: [
        "8dba19966d85ea2137505039c47d1e6ba35ab560797e51924fuedb939d9d2146"
      ],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

⚠️ Recommended: Follow this step-by-step guide to generate, configure and secure CrossFi API Key.

Step 5: Creating the smart contract

  • Navigate to the contracts directory and create a new file named Rental.sol
  • Populate the file with the following Solidity code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Rental {
    address owner;

    constructor() {
        owner = msg.sender;
    }

    // Add a new Renter
    struct Renter {
        address payable walletAddress;
        string firstName;
        string lastName;
        bool canRent;
        bool active;
        uint balance;
        uint amountDue;
        uint start;
        uint end;
    }

    // Mapping wallet addresses to each Renter
    mapping (address => Renter) public renters;

    // Function for adding a Renter to the list of "Renters"
    function addRenter(address payable walletAddress,
        string memory firstName,
        string memory lastName,
        bool canRent,
        bool active,
        uint balance,
        uint amountDue,
        uint start,
        uint end) public {
            renters[walletAddress] = Renter(payable (walletAddress), firstName, lastName, canRent, active, balance, amountDue, start, end);
    }

    // Checkout car ==> Rent a new car
    function checkOut(address payable walletAddress) public {
        require(renters[walletAddress].amountDue == 0, "You have a pending balance!");
        require(renters[walletAddress].canRent == true, "Can't rent a car at the moment!");
        renters[walletAddress].canRent = false;
        renters[walletAddress].active = true;
        renters[walletAddress].start = block.timestamp;
    }

    // Check-in car ==> Return rented car
    function checkIn(address payable walletAddress) public {
        require(renters[walletAddress].active == true, "Kindly checkout the car!");
        renters[walletAddress].active = false;
        renters[walletAddress].end = block.timestamp;

        // Set amount due
        setDueAmount(walletAddress);
    }

    // Calculate the Timespan
    function renterTimespan(uint start, uint end) internal pure returns (uint){
        return end - start;
    }

    // Get total duration of car use
    function getTotalDuration(address payable walletAddress) public view returns(uint) {
        require(renters[walletAddress].active == false, "The car is currently checked out!");
        uint timespan = renterTimespan(renters[walletAddress].start, renters[walletAddress].end);
        uint timespanInMinutes = timespan / 60;
        return timespanInMinutes;
    }

    // Get contract balance
    function balanceOf() public view returns (uint) {
        return address(this).balance;
    }

    // Get Renter's balance
    function balanceOfRenter(address payable walletAddress) public view returns(uint){
        return renters[walletAddress].balance;
    }

    // Set Due amount
    function setDueAmount(address payable  walletAddress) internal {
        uint timespanMinutes = getTotalDuration(walletAddress);
        uint fiveMinutesIncrement = timespanMinutes / 5;
        renters[walletAddress].amountDue = fiveMinutesIncrement * 5000000000000000;
    }

    // Reset renter's position after checking-in
    function canRentCar(address payable walletAddress) public view returns(bool) {
        return renters[walletAddress].canRent;
    }

    // Deposit fund
    function deposit(address walletAddress) payable public {
        renters[walletAddress].balance += msg.value;
    }

    // Make Payment after check-in
    function makePayment(address walletAddress) payable  public {
        require(renters[walletAddress].amountDue > 0, "You do not have anything due at this time!");
        require(renters[walletAddress].balance > 0, "You do not have enough fund to cover payment. Please make a deposit!");
        renters[walletAddress].balance -= msg.value;
        renters[walletAddress].canRent = true;
        renters[walletAddress].active = false;
        renters[walletAddress].amountDue = 0;
        renters[walletAddress].start = 0;
        renters[walletAddress].end = 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

About the smart contract

This Solidity smart contract implements a rental system, likely for renting cars, where users can check out and check in a vehicle while managing their balances and payments. Below is a breakdown of its components:

Contract Overview

The contract is named Rental and enables renters to:

  • Register themselves in the system.
  • Check out a car if they meet the required conditions.
  • Check in a car after use and calculate the rental cost.
  • Deposit funds to their account.
  • Make payments for the rental.
  • Finally, the contract keeps track of renter details, including their balance, rental status, and amount due.

Key Components

1. State variables

address owner;
Enter fullscreen mode Exit fullscreen mode
  • Stores the owner's address (the deployer of the contract).

2. Constructor

constructor() {
    owner = msg.sender;
}
Enter fullscreen mode Exit fullscreen mode
  • Assigns the deployer of the contract as the owner.

3. Renter Structure

struct Renter {
    address payable walletAddress;
    string firstName;
    string lastName;
    bool canRent;
    bool active;
    uint balance;
    uint amountDue;
    uint start;
    uint end;
}
Enter fullscreen mode Exit fullscreen mode
  • Defines a Renter with:
  • Wallet address (to track user payments).
  • Name (firstName and lastName).
  • Rental eligibility (canRent).
  • Current rental status (active).
  • Financial details (balance and amountDue).
  • Timestamps (start and end) to track rental duration.

4. Mapping Renters

mapping (address => Renter) public renters;
Enter fullscreen mode Exit fullscreen mode
  • A mapping that links wallet addresses to their corresponding Renter records.

5. Adding a Renter

function addRenter(
    address payable walletAddress,
    string memory firstName,
    string memory lastName,
    bool canRent,
    bool active,
    uint balance,
    uint amountDue,
    uint start,
    uint end) public {
        renters[walletAddress] = Renter(
            walletAddress, firstName, lastName, canRent, active, balance, amountDue, start, end);
}
Enter fullscreen mode Exit fullscreen mode
  • Allows adding a new renter with initial details.

6. Checking Out a Car

function checkOut(address payable walletAddress) public {
    require(renters[walletAddress].amountDue == 0, "You have a pending balance!");
    require(renters[walletAddress].canRent == true, "Can't rent a car at the moment!");

    renters[walletAddress].canRent = false;
    renters[walletAddress].active = true;
    renters[walletAddress].start = block.timestamp;
}
Enter fullscreen mode Exit fullscreen mode
  • Function ensures:
  • The renter does not have a pending balance.
  • The renter is eligible to rent.
  • Updates rental status and records the start time.

7. Checking In a Car

function checkIn(address payable walletAddress) public {
    require(renters[walletAddress].active == true, "Kindly checkout the car!");

    renters[walletAddress].active = false;
    renters[walletAddress].end = block.timestamp;

    // Calculate the due amount
    setDueAmount(walletAddress);
}
Enter fullscreen mode Exit fullscreen mode
  • Function ensures:
  • The renter has an active rental.
  • Updates rental status and records the end time.
  • Calls setDueAmount() function to calculate rental cost.

8. Calculating Rental Duration

function renterTimespan(uint start, uint end) internal pure returns (uint) {
    return end - start;
}
Enter fullscreen mode Exit fullscreen mode
  • Computes the duration in seconds between start and end.

9. Getting Total Rental Duration in Minutes

function getTotalDuration(address payable walletAddress) public view returns(uint) {
    require(renters[walletAddress].active == false, "The car is currently checked out!");

    uint timespan = renterTimespan(renters[walletAddress].start, renters[walletAddress].end);
    uint timespanInMinutes = timespan / 60;

    return timespanInMinutes;
}
Enter fullscreen mode Exit fullscreen mode
  • Converts the rental duration from seconds to minutes.
  • Ensures the renter has checked in.

10. Checking Contract Balance

function balanceOf() public view returns (uint) {
    return address(this).balance;
}
Enter fullscreen mode Exit fullscreen mode
  • Returns the total balance held by the contract.

11. Checking Renter's Balance

function balanceOfRenter(address payable walletAddress) public view returns(uint){
    return renters[walletAddress].balance;
}
Enter fullscreen mode Exit fullscreen mode
  • Returns the funds available in a renter's balance.

12. Setting the Amount Due

function setDueAmount(address payable walletAddress) internal {
    uint timespanMinutes = getTotalDuration(walletAddress);
    uint fiveMinutesIncrement = timespanMinutes / 5;

    renters[walletAddress].amountDue = fiveMinutesIncrement * 5000000000000000;
}
Enter fullscreen mode Exit fullscreen mode
  • Calculates the rental fee based on:
  • timespanMinutes / 5 → Rounds down to 5-minute blocks.
  • Each block costs 0.005 ETH (5000000000000000 wei).

13. Checking if a Renter Can Rent Again

function canRentCar(address payable walletAddress) public view returns(bool) {
    return renters[walletAddress].canRent;
}
Enter fullscreen mode Exit fullscreen mode
  • Returns true if the renter is allowed to rent.

14. Depositing Funds

function deposit(address walletAddress) payable public {
    renters[walletAddress].balance += msg.value;
}
Enter fullscreen mode Exit fullscreen mode
  • Allows renters to add funds to their balance.

15. Making Payment

function makePayment(address walletAddress) payable public {
    require(renters[walletAddress].amountDue > 0, "You do not have anything due at this time!");
    require(renters[walletAddress].balance > 0, "You do not have enough fund to cover payment. Please make a deposit!");

    renters[walletAddress].balance -= msg.value;
    renters[walletAddress].canRent = true;
    renters[walletAddress].active = false;
    renters[walletAddress].amountDue = 0;
    renters[walletAddress].start = 0;
    renters[walletAddress].end = 0;
}
Enter fullscreen mode Exit fullscreen mode
  • Function ensures:
  • The renter has an outstanding balance.
  • The renter has enough funds to cover the cost.
  • Deducts the amount from the balance and resets rental status.

Summary

  • Registration: addRenter()
  • Renting a car: checkOut()
  • Returning a car: checkIn()
  • Payment system:
  • setDueAmount() calculates cost.
  • deposit() adds funds.
  • makePayment() clears debts.
  • Tracking rental duration: getTotalDuration()
  • Checking balances: balanceOf(), balanceOfRenter()

Step 6: Compiling the smart contract

  • Add the directory where the automatically created ABI should be stored to the hardhat.config.cjs file
  paths: {
    artifacts: "./src/artifacts",
  }
Enter fullscreen mode Exit fullscreen mode
  • The modified hardhat.config.cjs file should look as follows:
require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.28",
  paths: {
    artifacts: "./src/artifacts",
  },
  networks: {
    crossfiTestnet: {
      chainId: 4157,
      url: "https://crossfi-testnet.g.alchemy.com/v2/wAz9j4RJUgEBiaMljD1yGbi45YBRKXTK",
      accounts: [
        "8dba19966d85ea2137505039c47d1e6ba35ab560797e51924fuedb939d9d2146",
      ],
    },
  },
}
Enter fullscreen mode Exit fullscreen mode
  • Execute the following command to compile the contract
yarn hardhat compile
Enter fullscreen mode Exit fullscreen mode

Step 7: Configuring the DApp for deployment

  • Create a new folder for deployment in the root directory
mkdir deploy
Enter fullscreen mode Exit fullscreen mode
  • Open the deploy folder and create a new file named 00-deploy-rental.cjs

  • Install another Hardhat plugin as a package for deployment

yarn add hardhat-deploy --dev
Enter fullscreen mode Exit fullscreen mode
  • Import hardhat-deploy package to the hardhat.config.cjs file
require("hardhat-deploy");
Enter fullscreen mode Exit fullscreen mode
  • Install hardhat-deploy-ethers to override the @nomiclabs/hardhat-ethers package
yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers
Enter fullscreen mode Exit fullscreen mode
  • Configure a deployer account in the hardhat.config.cjs file
networks: {
  // Code Here
},
namedAccounts: {
  deployer: {
    default: 0,
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Populate the 00-deploy-rental.cjs file with the following code:
module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy } = deployments;
  const { deployer } = await getNamedAccounts();
  await deploy("Rental", {
    contract: "Rental",
    from: deployer,
    args: [],
    log: true,
  });
};
module.exports.tags = ["Rental"];
Enter fullscreen mode Exit fullscreen mode

Step 8: Deploying the contract to CrossFi Testnet

yarn hardhat deploy --network crossfiTestnet
Enter fullscreen mode Exit fullscreen mode
  • The following should be the deployment output if the deployment is successful:
Compiled 1 Solidity file successfully (evm target: paris).
deploying "Rental" (tx: 0x722012ff0b9036f3d56587de9e87549be2b95e3b10a76525b22d489e34f72ffb)...: deployed at 0x0847857BE3dce76060Fabe41648CcbFe7f6898Fc with 1585543 gas
✨  Done in 15.76s.
Enter fullscreen mode Exit fullscreen mode
  • The Deployed at information provide the contract address
...: deployed at 0x0847857BE3dce76060Fabe41648CcbFe7f6898Fc
Enter fullscreen mode Exit fullscreen mode

Congratulations 🎉 You have successfully developed a decentralized application on the CrossFi test network.

Conclusion

In this tutorial, we explored how to develop a Rental DApp on CrossFi using Hardhat framework, covering smart contract creation and deployment. By leveraging CrossFi’s blockchain infrastructure, we created a decentralized rental system with seamless fund management.

As you continue refining your DApp, consider integrating frontend components with React, enhancing security with access control mechanisms, and optimizing gas fees. Experimenting with CrossFi’s features will further improve scalability and user experience.

Now that you have a solid foundation, what will you build next? 🚀

Top comments (0)