Create a tipping app with Solidity: Step-by-step Tutorial

Create a tipping app with Solidity: Step-by-step Tutorial

Solidity is the most popular programming language used in writing smart contracts on the blockchain. In this tutorial, you will get familiar with the basic concept of Solidity programming by building a smart contract that allows users to receive tips from fans, clients, and anyone with a blockchain wallet.

I will also introduce you to some terms and concepts as we code along. This article assumes you have a basic understanding of smart contracts. To follow along, you can use Remix online IDE or set up your local environment.

If you want a refresher course on Solidity, I wrote an amazing article that can bring you up to speed!

Creating our smart contract

First, we will start by declaring our compiler version and a bare-bone contract.

// SPDX-License-Identifier: MIT
//Author: Nnamdi Umeh - https://nnamdiumeh.dev
pragma solidity ^0.8.9;
contract TipX  {
    //smart contract code here
    uint counter = 0;
}

The first line indicates the license of our contract. The third line notifies the compiler what version it should use to compile our code, while the third line declares the contract.

  1. Declaring the data structs

//defining the structure of a Tip and Payment
struct Tip{
        uint id;
        address owner;
        string title;
        string description;
        uint maxAmount;
        bool isActive;
    }

    struct Payment{
        uint tipId;
        address owner;
        address customer;
        uint amount;
    }

We have both the Tip and Payment struct. Structs represent the structure of the data. For example, our Tip template will have an

  1. id with an unsigned integer type(unit),

  2. owner will be an address type,

  3. title and description are both text fields, hence, of string type,

  4. while isActive is a boolean value (true or false).

  5. Adding our mappings

//for storing all tips created by a user
    mapping(address => Tip[]) public allUserTips;   
    //for storing all payments received from a Tip
    mapping( uint => Payment[]) public tipPayments;
    //to get a tip
    mapping(uint => Tip) allTips;

Next, we'll add mappings. This is a data type that is more like an associative array in other programming languages. It uses a key to map a data value. In our code above, the allUserTips uses an address to store an array of the Tip struct.

To retrieve the map, we can simply call the variable with the key like so:

allUserTips[0x1e354cbFb451A6dD73902B504f05c5C66b43A3Eb] = Tip(dataValues)

  1. Events

event NewTipCreated(uint id, address indexed owner, string title, string description, uint maxAmount, bool isActive);
event NewPaymentReceived(uint tipId, address indexed owner, address indexed customer, uint amount);
event MakeWithdrawal(uint tipId, uint amount);

We then define events that fire on the blockchain when a particular function is called. For example, the event NewTipCreated will be fired whenever a new tip is added.

  1. Adding our functions

//create a new tip
function createNewTip(
    string memory _title, 
    string memory _description,
    uint _maxAmount) external {
    increment();
    //save new tip template to caller   
    allUserTips[msg.sender].push(Tip(
         counter,
         msg.sender,
         _title,
         _description,
         _maxAmount,
         true
     )); 
     //broadcast event
     emit NewTipCreated(counter, msg.sender, _title, _description, _maxAmount, true);
}

That was a little lengthy. But let's break it down:

  • The function accepts title, description and maxAmount. MaxAmount is the maximum amount that can be received as a tip.

  • It increases the counter state and assigns it as the id of the tip being created. It also records the tip in the allUserTips mapping. This mapping returns all the tips created by a user.

  • Finally, the function emits an event that fires on the blockchain so listeners can perform appropriate actions.

Next, we add a function that returns all tips created by a user.

//get all tips created by a user
function getUserTips() external view returns (  Tip[] memory ) {
    Tip[] memory userTips = new Tip[](allUserTips[msg.sender].length);
        for(uint i = 0; i < allUserTips[msg.sender].length; i++){
            userTips[i] = allUserTips[msg.sender][i];
        }
        return userTips;
    }

This function creates a variable, then gets all the user tips using the address as the key. Note that msg.sender is a global variable that is available in all smart contracts which return the function caller's address. The memory keyword means the variable will be saved to the memory and accessed throughout the lifetime of the function.

//get a single tip
function getTip(uint _id) public view returns (Tip memory){
    //check if Tip exists
    Tip memory tip = allTips[_id];
    require(tip.id > 0 , "Yikes! Not found on our records.");
     return allTips[_id];
}

The getTip function accepts an unsigned integer as a parameter, then uses it as a key to get the tip from allTips array and return the value.

Now that we have the basic functions to create Tips, we can now make a payable function that allows us to receive tips from donors.

//for giving tips
function giveTip(uint _tipId) external payable {
    Tip memory tip = getTip(_tipId);
    require(msg.value > 0, "Haba! you didn't send any tip.");
    require(msg.value <= tip.maxAmount, "Too kind. Exceeds amout");
    tipPayments[_tipId].push(Payment(
         _tipId,
         tip.owner,
         msg.sender,
         msg.value
      ));
     emit NewPaymentReceived(_tipId, tip.owner, msg.sender, 5);
    }

Note the payable keyword. This tells the compiler that this function can receive ethers. The function accepts a _tipId, do some checks to ensure that ether is received, then records the tip and fire NewPaymentReceived event on the blockchain.

Lastly, we add the Withdraw function. This function allows the creator of a Tip template to withdraw accumulated tip payments. The payments received are sent to the creator's wallet, and the Tip template is deactivated.

 //withdraw funds and deactivate Tip
function withdrawMoney(uint _tipId) external payable {
    Tip memory tip = getTip(_tipId);
    uint amount = getTipTotal(_tipId);
    //checks if tip is active
    require(tip.isActive == true, "Tips is already withdrawn");
    //checks if caller is owner
    require(tip.owner == msg.sender, "No games please");
    (bool success, ) = payable(msg.sender).call{value: amount}("");
    require(success, "Transfer failed.");

    //deactivate Tip
    allUserTips[tip.owner][_tipId - 1].isActive = false;
    emit MakeWithdrawal(_tipId, amount);
    }

Conclusion

If you made it up to this point, you'd have had a better understanding of how smart contract pieces work together to achieve a common goal. Our smart contract is ready to be deployed and used for helping creators accept tips. There are quite several ways to deploy our contract, namely:

  1. Through Remix IDE

  2. Through your development framework. that is, Truffle or HardHat

  3. Through Bunzz. Bunzz is a Smart contract-as-a-service platform that allows users to deploy smart contracts quickly by just uploading the contracts folder. It also allows you to build DApps faster by building on already audited smart contracts which cover most use cases.

This code is limited and used here, as a reference for learning. For the full code, you can check it out on GitHub. I also have the dApp running on Sepolia testnet.

Thanks for reading, and hopefully, coding along. If you have any questions, do not hesitate to hit the comment or ask me on Twitter.