How To Build A Ride-Hailing Smart Contract With Solidity

Ride-hailing businesses such as Uber and InDrive are fast becoming interwoven into modern society. They are a million-dollar industry.

In this tutorial, I built a smart contract for a ride-hailing service company with Solidity and tested it locally with Remix.

If you have been playing around with Solidity for some time now or are a proper Solidity developer, you will pick a few things from this tutorial.

In explaining how I built the contract, I will try not to go too high so everyone can understand the reasoning behind the code and engineering choices made along the way.

TL:DR

// SPDX-License-Identifier: MIT
// this is mainly for education. it is not robust enough and its security is not guaranteed.

 pragma solidity 0.8.25;

  contract OnChainUber{

    struct TripDetails{
        address payable driverAddress;
        address payable clientAddress;
        uint256 tripID;
        uint256 fare;
        bool started;
        uint timestamp;
        bool done;
    }

    struct Client{
        string clientName;
        address payable clientAddress;
        string location;
        string destination;
        uint256[] clientTripIDs;
    }

    struct Driver{
        string driverName;
        string carModel;
        bool availability;
        address payable driverAddress;
        uint256[] driverTripIDs;
    }

    struct Reviews{
      string comment;
      string name;
    }

    Reviews[] internal reviews;

// the maps

   mapping(uint => TripDetails) internal tripMaps;
   mapping(address => Client) internal clientMaps;
   mapping(address => Driver) internal driverMaps;
   uint256 totalTrips;

// events
   event DriverProfileCreated(address driver, string driverName, string carModel, bool availability);
   event ClientProfileCreated(address client, string clientName, string location, string destination);
   event TripBooked(address driver, address client, uint timestamp, uint id, uint fare, bool started, bool done);
   event TripCompleted(address clientThatPaid, uint fare, bool done, uint timestamp);
   event ReviewDropped(string review, string name);

  //emit TripCompleted(msg.sender, msg.value, destinationReached.done, destinationReached.timestamp);



// setting up profiles

  // for the drivers

   function createDriverProfile(address _driverAddress, string memory _driverName, string memory _carModel) external payable{
        require(_driverAddress != address(0), "invalid address");


        Driver storage DriverProfile = driverMaps[msg.sender];

        DriverProfile.driverAddress = payable (_driverAddress);
        DriverProfile.driverName = _driverName;
        DriverProfile.carModel = _carModel;
        DriverProfile.availability = true;

        emit DriverProfileCreated(DriverProfile.driverAddress, DriverProfile.driverName, DriverProfile.carModel, DriverProfile.availability);
     }
   // for the clients

     function createClientProfile(address _clientAddress, string memory _name, string memory _location, string memory _destination) external{
        require(_clientAddress != address(0), "invalid address");

      Client storage ClientProfile = clientMaps[msg.sender];

      ClientProfile.clientAddress = payable (_clientAddress);
      ClientProfile.clientName = _name;
      ClientProfile.location = _location;
      ClientProfile.destination = _destination;

      emit ClientProfileCreated(ClientProfile.clientAddress, ClientProfile.clientName, ClientProfile.location, ClientProfile.destination);

     }

   //   function getAvailableDrivers() external returns (bool){
   //      Driver storage DriverAvailability = driverMaps[msg.sender];
   //      return(DriverAvailability.availability = true);
   //   }

     function BookTrip() external payable{
        TripDetails storage currentTrip = tripMaps[totalTrips];

        require(msg.value > 0, "the transport fare cannot be less than 0");

        currentTrip.driverAddress = payable(msg.sender);
        currentTrip.clientAddress = payable(msg.sender);
        currentTrip.tripID = totalTrips;
        currentTrip.fare = msg.value;
        currentTrip.timestamp = block.timestamp;
        currentTrip.started = true;
        currentTrip.done = false;

        totalTrips++;

        emit TripBooked(currentTrip.driverAddress, currentTrip.clientAddress, currentTrip.timestamp, currentTrip.tripID, currentTrip.fare, currentTrip.started, currentTrip.done);
     }

     function PayForCompletedTrip(uint256 _tripID) external payable{
      require(msg.value > 0.000002 ether, "a ride goes for 0.000003 and above, nothing less");

      TripDetails storage destinationReached = tripMaps[_tripID];

      destinationReached.done = false;
      destinationReached.timestamp = block.timestamp;

      (bool sent, bytes memory data) = destinationReached.driverAddress.call{value: msg.value}("");
      require(sent, "payment not successful, kindly try again");

      Client storage hiredCar = clientMaps[msg.sender];
      hiredCar.clientTripIDs.push(_tripID);

      emit TripCompleted(msg.sender, msg.value, destinationReached.done, destinationReached.timestamp);

     }



// for both to drop reviews

  function TripReview(string memory _comment, string memory _name) external{

        reviews.push(Reviews(_comment, _name));

        emit ReviewDropped(_comment, _name);
     }

  function getTripReviews() public view returns (Reviews[] memory) {
    return reviews;
  }


  }

Business Logic

I did not start writing the code straight away. Instead, I started by planning how the DApp should work and the essential features it needs.

This is why I am also writing this section. So before going into the code, you have an engineering perception of the code, how it works, and the trade-offs.

First, the contract is owned by a hypothetical ride-hailing company. The company links clients with drivers within their vicinities.

Both clients and drivers can create profiles on the DApp. Only the clients can book a trip as that is the logical process.

However, the client doesn't need to pay with the address they used to sign up for the DApp.

Any address can pay for any trip. I built the contract this way for edge cases where clients might be unable to make the transfers themselves. At least they can call someone to pay for them.

Clients and drivers can also leave reviews after a ride. They will input their name and any comments they have.

I didn’t serialize these comments with the sequence of trips as the company mainly needs all the feedback to improve its product.

Now that you have a broad idea of the business we are building with the contract, let’s start with the actual guide.

Step-by-step Guide to Building a Ride-hailing Smart Contract with Solidity

To participate in this tutorial, ensure your VS Code is set up. Or you can use Remix, which is sufficient for what we are building.

Step 1: Creation of Structs

In Solidity, we use structs to pack custom data and call them by a name we can later initialize. We will create four structs for trip details, clients, drivers, and reviews.

Here is the struct for trip details:

// SPDX-License-Identifier: MIT

// this is mainly for education. it is not robust enough and its security is not guaranteed.

 pragma solidity 0.8.25;

  contract OnChainUber{



    struct TripDetails{

        address payable driverAddress;

        address payable clientAddress;

        uint256 tripID;

        uint256 fare;

        bool started;

        uint timestamp;

        bool done;

    }

We created the contract as usual and specified the 0.8.25 version of Solidity. We named the contract OnChainUber as well. Down to the struct named TripDetails, we included these details:

  • The wallet address of the driver

  • The wallet address of the client

  • The ID of the trip

  • The timestamp shows whenever the trips start or end

  • The money to be paid after the service - fare

  • The boolean types for start and end

This is the code for the second struct:

    struct Client{

        string clientName;

        address payable clientAddress;

        string location;

        string destination;

        uint256[] clientTripIDs;

    }

We specified some important details that an ideal client should provide. These include the name, address, location, destination, and the IDs of any trips they have taken in the past.

Moving on to the struct for the drivers:

   struct Driver{

        string driverName;

        string carModel;

        bool availability;

        address payable driverAddress;

        uint256[] driverTripIDs;

    }

It encompassed the driver’s name, their address, and the model of the car they drive. It also contains a bool type that shows if they are currently available or not and a data type to know the number of trips the driver has made on the platform.

The last struct is for reviews:

    struct Reviews{

      string comment;

      string name;

    }

Reviews[] internal reviews;

Both the drivers and clients can drop reviews alongside their names. We immediately created an array to contain the reviews.

Step 2: Creation of Mappings

We need mappings to track how a lot of activities happen in the contract.

  mapping(uint => TripDetails) internal tripMaps;

   mapping(address => Client) internal clientMaps;

   mapping(address => Driver) internal driverMaps;

   uint256 totalTrips;

The essence of the first mapping is to track each trip. The second and third ones track the clients and drivers, respectively.

After the mappings, we have to create a data type to signify all the trips, which will be useful later in this guide.

Step 3: Creating the Drivers’ Profile

Every driver is onboarded to the DApp whenever they create a profile.

function createDriverProfile(address driverAddress, string memory driverName, string memory _carModel) external payable{

        require(_driverAddress != address(0), "invalid address");



        Driver storage DriverProfile = driverMaps[msg.sender];

        DriverProfile.driverAddress = payable (_driverAddress);

        DriverProfile.driverName = _driverName;

        DriverProfile.carModel = _carModel;

        DriverProfile.availability = true;

        emit DriverProfileCreated(DriverProfile.driverAddress, DriverProfile.driverName, DriverProfile.carModel, DriverProfile.availability);

     }

In this function, we put a condition that disallows zero addresses to protect us against spam and attacks.

We initialized the Driver struct as DriverProfile, which we defined by using the mapping of drivers to shape the msg.sender.

We assigned each useful object of the Driver struct a new name, which we also defined locally in the function's parameters. Thus, any driver-to-be will have to input the required details as arguments for the function to work.

Step 4: Creating the Client's Profile

This function makes it possible for clients to create their profiles:

     function createClientProfile(address clientAddress, string memory name, string memory location, string memory destination) external{

        require(_clientAddress != address(0), "invalid address");

      Client storage ClientProfile = clientMaps[msg.sender];

      ClientProfile.clientAddress = payable (_clientAddress);

      ClientProfile.clientName = _name;

      ClientProfile.location = _location;

      ClientProfile.destination = _destination;

      emit ClientProfileCreated(ClientProfile.clientAddress, ClientProfile.clientName, ClientProfile.location, ClientProfile.destination);

     }

The main details this function collects for the clients are their address, name, location, and destination.

Step 5: Booking a Ride

Clients can book a ride. We marked this function as payable because we wanted to test if any address that wants to book a ride, it must have more than 0 ether.

     function BookTrip() external payable{

        TripDetails storage currentTrip = tripMaps[totalTrips];

        require(msg.value > 0 ether, "the transport fare cannot be less than 0");

        currentTrip.driverAddress = payable(msg.sender);

        currentTrip.clientAddress = payable(msg.sender);

        currentTrip.tripID = totalTrips;

        currentTrip.fare = msg.value;

        currentTrip.timestamp = block.timestamp;

        currentTrip.started = true;

        currentTrip.done = false;

        totalTrips++;

        emit TripBooked(currentTrip.driverAddress, currentTrip.clientAddress, currentTrip.timestamp, currentTrip.tripID, currentTrip.fare, currentTrip.started, currentTrip.done);

     }

We contextualized the objects of the TripDetails struct with this function. Then we incremented the totalTrips type to keep increasing whenever anyone books a trip.

Step 6: Paying for a Trip

     function PayForCompletedTrip(uint256 _tripID) external payable{

      require(msg.value > 0.000002 ether, "a ride goes for 0.000003 and above, nothing less");



      TripDetails storage destinationReached = tripMaps[_tripID];

      destinationReached.done = false;

      destinationReached.timestamp = block.timestamp;

      (bool sent, bytes memory data) = destinationReached.driverAddress.call{value: msg.value}("");

      require(sent, "payment not successful, kindly try again");

      Client storage hiredCar = clientMaps[msg.sender];

      hiredCar.clientTripIDs.push(_tripID);

      emit TripCompleted(msg.sender, msg.value, destinationReached.done, destinationReached.timestamp);

     }

The payment for each trip is 0.000002 ether, so we set a condition that nothing less than such amount can suffice as the fare. We did 3 other things in this function:

  1. We marked the trip as done and also recorded when it ended

  2. We transferred the fare to the driver

  3. We added the trip to the history of the client on the platform

Step 7: Feedback Collection

The contract was designed to ideally receive feedback from clients and drivers. Without prejudice, practically anyone can drop reviews or feedback.

  function TripReview(string memory comment, string memory name) external{



        reviews.push(Reviews(_comment, _name));

        emit ReviewDropped(_comment, _name);

     }

  function getTripReviews() public view returns (Reviews[] memory) {

    return reviews;

  }

The first function leveraged the Reviews array we had created at the state, and we also specified the type of responses we wanted to pull, which are names and comments.

Then, we pushed whatever was obtainable into the array. The second function is a getter function that returns the reviews. You can get the full code here or return to the TL;DR above.

Step 8: Creation of Events

On the frontend side, events help us communicate major on-chain happenings, so we emit these events within the functions.

emit DriverProfileCreated(DriverProfile.driverAddress, DriverProfile.driverName, DriverProfile.carModel, DriverProfile.availability);

emit ClientProfileCreated(ClientProfile.clientAddress, ClientProfile.clientName, ClientProfile.location, ClientProfile.destination);

emit TripBooked(currentTrip.driverAddress, currentTrip.clientAddress, currentTrip.timestamp, currentTrip.tripID, currentTrip.fare, currentTrip.started, currentTrip.done);

emit TripCompleted(msg.sender, msg.value, destinationReached.done, destinationReached.timestamp);

        emit ReviewDropped(_comment, _name);

We also declared them at the state:

   event DriverProfileCreated(address driver, string driverName, string carModel, bool availability);

   event ClientProfileCreated(address client, string clientName, string location, string destination);

   event TripBooked(address driver, address client, uint timestamp, uint id, uint fare, bool started, bool done);

   event TripCompleted(address clientThatPaid, uint fare, bool done, uint timestamp);

   event ReviewDropped(string review, string name);

Testing the Contract with Remix

We will go ahead and test the code with Remix:

Step 1: Compilation and Deployment

Go ahead to compile the contracts here:

It could have even automatically compiled itself. Then deploy with this button:

Other things being equal, the contract should have been deployed.

Step 2 Creating Profiles

Creating profiles is simple. Input all the prompted details. Be careful to use different addresses for the driver and client, respectively.

Click on transact when you are done.

Step 3: Booking a Ride

Click on the BookTrip button.

Step 4: Payment

To pay, fill your wallet with 1 ether.

Then, input the ID and click on transact.

Step 5: Dropping Reviews

You can test feedback by filling in the review and name parameters with words.

Step 6: Accessing all the Feedback

You can call the getTripReviews function by clicking on this button.

I was also thinking of showing you how to test it with Foundry. You can drop a comment if you’d like to see that.

What Next?

If you followed all the steps in this tutorial, you just built and deployed an on-chain Uber. Kudos! Let me know if anything in the tutorial is not clear.

While you are here, you can follow me on Twitter!

***

Blockchain Alpha is a Web3 technical content marketing company. We are your best technical content marketing partner if you are building a developer-focused product in Web3.

It’s simple: book a call with our Manager.