Build an RSVP Decentralized application on CELO: Backend and Subgraph

Build an RSVP Decentralized application on CELO: Backend and Subgraph https://celo.academy/uploads/default/optimized/2X/2/28bd2fd49020f2004181bde159c6f7a92a1df7e0_2_1024x576.jpeg
none 0.0 0

About the tutorial

In this tutorial, we shall be building an RSVP decentralized application (dApp) on CELO to organize the process of sending, receiving, and tracking RSVPs for events. One important use case for this is for large-scale events such as conferences and concerts where attendees are required to register and RSVP ahead so that organizers can manage their registrations and track their attendance.

Prerequisites

Before you begin this tutorial, kindly ensure you have all the necessary tools installed. The following should be installed on your computer

  • Node.js
  • NPM (Node Package Manager)

Starter Guide

For a starter guide, you can check out the codebase here

I recommend you take a look at the env.sample file in the code as it gives you a hint of what should be in your environment variables

Celo Composer

Celo Composer is built on Celo to make it simple to build dApps using a variety of front-end frameworks, and libraries. For more information on how to start off your project with Celo Composer, visit here. Please keep in mind that these are the frameworks you should select while installing Celo Composer

  • React
  • Rainbow-kit celo
  • Hardhat
  • Subgraph

If you are stuck, kindly refer to the Celo Composer installation section of my previous tutorial here.

The tiny difference is that when asked if you need to use a subgraph, please choose yes

Smart Contract

Once your dApp is up, navigate to the packages>> hardhat >> contract folder and create an Rsvp.sol file and copy this starter code

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

contract RSVP {
    event NewEventCreated(
        bytes32 eventId,
        address creator,
        uint256 eventTime,
        uint256 capacity,
        uint256 depositAmount,
        string eventDataHash
    );

    event NewRSVP(bytes32 eventId, address attendee);

    event AttendeeConfirmed(bytes32 eventId, address attendee);

    event DepositsPaidOut(bytes32 eventId);

    struct Event {
        bytes32 eventId;
        string eventDataHash;
        address eventOwner;
        uint256 eventTime;
        uint256 depositAmount;
        uint256 maxCapacity;
        address[] confirmedRSVPs;
        address[] claimedRSVPs;
        bool paidOut;
    }

    mapping(bytes32 => Event) public events;

    function createNewEvent(
        uint256 eventTime,
        uint256 depositAmount,
        uint256 maxCapacity,
        string calldata eventDataHash
    ) external {
        // generate an event ID based on other variables to create a hash
        bytes32 eventId = keccak256(
            abi.encodePacked(
                msg.sender,
                address(this),
                eventTime,
                depositAmount,
                maxCapacity
            )
        );

        address[] memory confirmedRSVPs;
        address[] memory claimedRSVPs;

        // create a new Event struct and add it to the events mapping
        events[eventId] = Event(
            eventId,
            eventDataHash,
            msg.sender,
            eventTime,
            depositAmount,
            maxCapacity,
            confirmedRSVPs,
            claimedRSVPs,
            false
        );

        emit NewEventCreated(
            eventId,
            msg.sender,
            eventTime,
            maxCapacity,
            depositAmount,
            eventDataHash
        );
    }

    function createNewRSVP(bytes32 eventId) external payable {
        // look up the event
        Event storage myEvent = events[eventId];

        // transfer the deposit to our contract and require that enough ETH was sent
        require(msg.value == myEvent.depositAmount, "INSUFFICIENT FUNDS");

        // require that the event hasn't already happened
        require(block.timestamp <= myEvent.eventTime, "ALREADY HAPPENED");

        // make sure the event is under the maximum capacity
        require(
            myEvent.confirmedRSVPs.length < myEvent.maxCapacity,
            "EVENT AT CAPACITY"
        );

        // require that msg.sender isn't already in myEvent.confirmedRSVPs
        for (uint8 i = 0; i < myEvent.confirmedRSVPs.length; i++) {
            require(myEvent.confirmedRSVPs[i] != msg.sender, "ALREADY RSVP'D");
        }

        myEvent.confirmedRSVPs.push(payable(msg.sender));

        emit NewRSVP(eventId, msg.sender);
    }

    function confirmAllAttendees(bytes32 eventId) external {
        // look up the event
        Event memory myEvent = events[eventId];

        // require that msg.sender is the owner of the event
        require(msg.sender == myEvent.eventOwner, "NOT AUTHORIZED");

        // confirm each attendee
        for (uint8 i = 0; i < myEvent.confirmedRSVPs.length; i++) {
            confirmAttendee(eventId, myEvent.confirmedRSVPs[i]);
        }
    }

    function confirmAttendee(bytes32 eventId, address attendee) public {
        // look up the event
        Event storage myEvent = events[eventId];

        // require that msg.sender is the owner of the event
        require(msg.sender == myEvent.eventOwner, "NOT AUTHORIZED");

        // require that attendee is in myEvent.confirmedRSVPs
        address rsvpConfirm;

        for (uint8 i = 0; i < myEvent.confirmedRSVPs.length; i++) {
            if (myEvent.confirmedRSVPs[i] == attendee) {
                rsvpConfirm = myEvent.confirmedRSVPs[i];
            }
        }

        require(rsvpConfirm == attendee, "NO RSVP TO CONFIRM");

        // require that attendee is NOT in the claimedRSVPs list
        for (uint8 i = 0; i < myEvent.claimedRSVPs.length; i++) {
            require(myEvent.claimedRSVPs[i] != attendee, "ALREADY CLAIMED");
        }

        // require that deposits are not already claimed
        require(myEvent.paidOut == false, "ALREADY PAID OUT");

        // add them to the claimedRSVPs list
        myEvent.claimedRSVPs.push(attendee);

        // sending eth back to the staker https://solidity-by-example.org/sending-ether
        (bool sent, ) = attendee.call{value: myEvent.depositAmount}("");

        //if this fails
        if (!sent) {
            myEvent.claimedRSVPs.pop();
        }

        require(sent, "Failed to send Ether");

        emit AttendeeConfirmed(eventId, attendee);
    }

    function withdrawUnclaimedDeposits(bytes32 eventId) external {
        // look up event
        Event memory myEvent = events[eventId];

        // check if already paid
        require(!myEvent.paidOut, "ALREADY PAID");

        // check if it's been 7 days past myEvent.eventTimestamp
        require(block.timestamp >= (myEvent.eventTime + 7 days), "TOO EARLY");

        // only the event owner can withdraw
        require(msg.sender == myEvent.eventOwner, "MUST BE EVENT OWNER");

        // calculate how many people didn't claim by comparing
        uint256 unclaimed = myEvent.confirmedRSVPs.length -
            myEvent.claimedRSVPs.length;

        uint256 payout = unclaimed * myEvent.depositAmount;

        // mark as paid before sending to avoid reentrancy attack
        myEvent.paidOut = true;

        // send the payout to the owner
        (bool sent, ) = msg.sender.call{value: payout}("");

        // if this fails
        if (!sent) {
            myEvent.paidOut = false;
        }

        require(sent, "Failed to send Ether");

        emit DepositsPaidOut(eventId);
    }
}

This smart contract will allow event owners create events with a certain capacity (number of attendees) and deposit amount, Attendees will then RSVP to those events by sending that deposit amount.

Proceed to write your deployment scripts. Navigate to packages/hardhat/contracts. Firstly, you need to install hardhat. Run this command to do so

npm i hardhat && npx hardhat compile

Your final output should look like this

x Solidity files compiled

In the deploy folder, create a script file. You could name it deploy.js and write your deployment code. Your code might look similar to this

const hre = require("hardhat");

const main = async () => {
  const contractFactory = await hre.ethers.getContractFactory('RSVP');
  const contract = await contractFactory.deploy();
  await contract.deployed();
  console.log("Contract deployed to this address:", contract.address);
  
};

const runMain = async () => {
  try {
    await main();
    process.exit(0);
  } catch (error) {
    console.log(error);
    process.exit(1);
  }
};

runMain();

Go back to your terminal and run the following command

npx hardhat --network alfajores run deploy/deploy.js

Your output should look similar to this

Contract deployed to this address: 0xc85CA226772cA949801EfCA6B9d153fB4a833aF7

Finally, verify your contract address with this command

npx hardhat --network alfajores verify 0xc85CA226772cA949801EfCA6B9d153fB4a833aF7

This is what your output should look like after the verification

Nothing to compile
Successfully submitted source code for contract
contracts/Rsvp.sol:RSVP at 0xc85CA226772cA949801EfCA6B9d153fB4a833aF7
for verification on the block explorer. Waiting for verification result...

Successfully verified contract RSVP on Etherscan.
https://alfajores.celoscan.io/address/0xc85CA226772cA949801EfCA6B9d153fB4a833aF7#code

Note: You need to have an etherscan apikey for a successful verification. You can get your API key from etherscan. Just login and generate one. In your hardhatconfig, please check that you have this variable
etherscan: {
apiKey: {
alfajores: process.env.CELOSCAN_API_KEY,
celo: process.env.CELOSCAN_API_KEY,
},
},

Subgraph

If you aren’t familiar with what a subgraph is, it is a blockchain indexing tool built by The Graph. The good thing is that it is part of the tools that can be added when setting up using Celo Composer. You should have had that set up earlier. Visit the hosted service on the Graph to create a subgraph. Sign in with your Github account

Select My Dasboard and Add Subgraph

You will be asked to fill in some information about the subgraph. I named my subgraph RSVP

Then proceed to click on the Create button. Once that’s done, you will be give some command templates such as for initialization , building, code generation and authentication.

Navigate to the packages/subgraphs folder and run the initialize command from your subgraph account. For mine, this was my command

graph init --product hosted-service amoweolubusayo/rsvp

You should select ethereum

Also choose hosted service

You will also be asked which specific ethereum service you want to work with. Select celo-alfajores

After this, you will be asked for the contract address you want to index. Provide this in the terminal as well

And with this, you have initialize your subgraph project.

For a sneak peek of how a working folder should look like, check out RSVP-composer/packages/subgraphs at main · amoweolubusayo/RSVP-composer · GitHub. Do not copy any build or generated folders.

Still in the packages/subgraphs folder, make the following changes to the schema.graphql file. No worries if you don’t have it, create one in that root folder. For starters, you can use this code

type Event @entity {
  id: ID!
  eventId: Bytes!
  name: String
  description: String
  link: String
  imageURL: String
  creator: Bytes!
  eventTime: BigInt!
  maxCapacity: BigInt!
  depositAmount: BigInt!
  paidOut: Boolean!
  totalRSVPs: BigInt!
  totalConfirmedAttendees: BigInt!
  rsvps: [RSVP!] @derivedFrom(field: "event")
  confirmedAttendees: [Confirmation!] @derivedFrom(field: "event")
}

type Account @entity {
  id: ID!
  totalRSVPs: BigInt!
  totalAttendedEvents: BigInt!
  rsvps: [RSVP!] @derivedFrom(field: "attendee")
  attendedEvents: [Confirmation!] @derivedFrom(field: "attendee")
}

type RSVP @entity {
  id: ID!
  attendee: Account!
  event: Event!
}

type Confirmation @entity {
  id: ID!
  attendee: Account!
  event: Event!
}

In my own project, my smart contract’s abi was created in the abi/alfajores folder. If by any chance, yours didn’t get created at all in any folder, no need to panic, you can create any folder of your choice and create a file to hold the abi of your smart contract. So the file could be named RSVP.json . Go to your smart contract on alfajores explorer and copy the abi. The images below will guide you as to where to find that

Copy the contract abi to your RSVP.json file.

Also , we need to make changes to our subgraph.yaml file. Again, if you don’t have it for an reason, just create one in the root folder same place the schema.graphql file is. Mine looks like this so properly reference the folder/file where your abi is. For example, mine is in the abis/alfajores folder. Check your deployed smart contract in the explorer for the startBlock as well. You will find this in the transactions tab. Mine was created at 17702677

specVersion: 0.0.4
features:
  - ipfsOnEthereumContracts
schema:
  file: ./schema.graphql
dataSources:
  - kind: ethereum
    name: RSVP
    network: celo-alfajores
    source:
      address: "0xc85CA226772cA949801EfCA6B9d153fB4a833aF7"
      abi: RSVP
      startBlock: 17702677
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.6
      language: wasm/assemblyscript
      entities:
        - Event
        - Account
        - RSVP
        - Confirmation
      abis:
        - name: RSVP
          file: ./abis/alfajores/RSVP.json
      eventHandlers:
        - event: AttendeeConfirmed(bytes32,address)
          handler: handleConfirmedAttendee
        - event: DepositsPaidOut(bytes32)
          handler: handleDepositsPaidOut
        - event: NewEventCreated(bytes32,address,uint256,uint256,uint256,string)
          handler: handleNewEventCreated
        - event: NewRSVP(bytes32,address)
          handler: handleNewRSVP
      file: ./src/mapping.ts

Proceed to run the code generation command. Run this command

graph codegen

It will create a generated folder and/ or a mapping file which we will be working with shortly.

Proceed to the src folder or the folder where your mapping file got created. You should find a code template with the handlers you have in your subgraph.yaml file as functions. You need to map out the data you want from the subgraph in this file. So this code is a very good starting point.

import { Address, ipfs, json } from "@graphprotocol/graph-ts";
import {
  AttendeeConfirmed,
  NewEventCreated,
  NewRSVP,
  DepositsPaidOut,
} from "../generated/RSVP/RSVP";
import { Account, RSVP, Confirmation, Event } from "../generated/schema";
import { integer } from "@protofire/subgraph-toolkit";

export function handleNewEventCreated(event: NewEventCreated): void {
  let newEvent = Event.load(event.params.eventId.toHex());
  if (newEvent == null) {
    newEvent = new Event(event.params.eventId.toHex());
    newEvent.eventID = event.params.eventId;
    newEvent.eventOwner = event.params.creator;
    newEvent.eventTimestamp = event.params.eventTime;
    newEvent.maxCapacity = event.params.capacity;
    newEvent.deposit = event.params.depositAmount;
    newEvent.paidOut = false;
    newEvent.totalRSVPs = integer.ZERO;
    newEvent.totalConfirmedAttendees = integer.ZERO;

    let metadata = ipfs.cat(event.params.eventDataHash + "/data.json");

    if (metadata) {
      const value = json.fromBytes(metadata).toObject();
      if (value) {
        const name = value.get("name");
        const description = value.get("description");
        const link = value.get("link");
        const imagePath = value.get("image");

        if (name) {
          newEvent.name = name.toString();
        }

        if (description) {
          newEvent.description = description.toString();
        }

        if (link) {
          newEvent.link = link.toString();
        }

        if (imagePath) {
          const imageURL =
            "https://ipfs.io/ipfs/" +
            event.params.eventDataHash +
            imagePath.toString();
          newEvent.imageURL = imageURL;
        } else {
          const fallbackURL =
            "https://ipfs.io/ipfs/bafybeibssbrlptcefbqfh4vpw2wlmqfj2kgxt3nil4yujxbmdznau3t5wi/event.png";
          newEvent.imageURL = fallbackURL;
        }
      }
    }

    newEvent.save();
  }
}

function getOrCreateAccount(address: Address): Account {
  let account = Account.load(address.toHex());
  if (account == null) {
    account = new Account(address.toHex());
    account.totalRSVPs = integer.ZERO;
    account.totalAttendedEvents = integer.ZERO;
    account.save();
  }
  return account;
}

export function handleNewRSVP(event: NewRSVP): void {
  let id = event.params.eventId.toHex() + event.params.attendee.toHex();
  let newRSVP = RSVP.load(id);
  let account = getOrCreateAccount(event.params.attendee);
  let thisEvent = Event.load(event.params.eventId.toHex());
  if (newRSVP == null && thisEvent != null) {
    newRSVP = new RSVP(id);
    newRSVP.attendee = account.id;
    newRSVP.event = thisEvent.id;
    newRSVP.save();
    thisEvent.totalRSVPs = integer.increment(thisEvent.totalRSVPs);
    thisEvent.save();
    account.totalRSVPs = integer.increment(account.totalRSVPs);
    account.save();
  }
}

export function handleConfirmedAttendee(event: AttendeeConfirmed): void {
  let id = event.params.eventId.toHex() + event.params.attendee.toHex();
  let newConfirmation = Confirmation.load(id);
  let account = getOrCreateAccount(event.params.attendee);
  let thisEvent = Event.load(event.params.eventId.toHex());
  if (newConfirmation == null && thisEvent != null) {
    newConfirmation = new Confirmation(id);
    newConfirmation.attendee = account.id;
    newConfirmation.event = thisEvent.id;
    newConfirmation.save();
    thisEvent.totalConfirmedAttendees = integer.increment(
      thisEvent.totalConfirmedAttendees
    );
    thisEvent.save();
    account.totalAttendedEvents = integer.increment(
      account.totalAttendedEvents
    );
    account.save();
  }
}

export function handleDepositsPaidOut(event: DepositsPaidOut): void {
  let thisEvent = Event.load(event.params.eventId.toHex());
  if (thisEvent) {
    thisEvent.paidOut = true;
    thisEvent.save();
  }
}

We introduced two new libraries here, so you can install them by running these commands

npm i @graphprotocol/graph-ts
npm i @protofire/subgraph-toolkit

This is all the code we need. So proceed to build and deploy

Run this command

graph build

Your output should look like this

Go back to your subgraph and pick your auth command, without it you won’t be able to deploy. Run the auth command

graph auth --product hosted-service YOUR_KEY

Expect this output

Finally, run the deploy command

graph deploy --product hosted-service amoweolubusayo/rsvp

Your output should look like this

Go back to your subgraph dashboard to see changes

We did it! You did it!

Conclusion

I am glad you made it this far with this tutorial! You were able to create smart contract, deploy and verify for our usecase. You also learnt how to create a subgraph , build and deploy as well! This doesn’t end here as I am sure you are interested in seeing how this works with a UI. In my next tutorial, I will teach you how to build a front-end interface that implements all we have learnt here.

4 Likes

I will be reviewing this.

1 Like

Approved @Kunaldawar

1 Like

Thank you! @Kunaldawar

2 Likes

An amazing content right here , very detailed.

1 Like

Great piece🙌

A nice tutorial I must say.

1 Like

Good work

2 Likes

Nice work, looking forward to this