---
# System prepended metadata

title: Build an RSVP Decentralized application on CELO

---

# Build an RSVP Decentralized application on CELO

# 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](https://github.com/amoweolubusayo/RSVP-composer)

> 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](https://github.com/celo-org/celo-composer). 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](https://docs.celo.org/blog/tutorials/creating-a-charity-donation-dApp-using-celo-composer-and-react-and-rainbowKit-celo). 
>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

```solidity
// 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

```javascript
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

```bash
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

```bash
npx hardhat --network alfajores verify 0xc85CA226772cA949801EfCA6B9d153fB4a833aF7
```
This is what your output should look like after the verification
```bash
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](https://thegraph.com/docs/en/network/explorer). 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](https://thegraph.com/hosted-service) on the Graph to create a subgraph. Sign in with your Github account

![](https://hackmd.io/_uploads/BJWYlf24h.png)

Select My Dasboard and Add Subgraph

![](https://hackmd.io/_uploads/r1NAbM2Nn.png)

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

![](https://hackmd.io/_uploads/S13Dta3En.png)

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
![](https://hackmd.io/_uploads/ByJU0W2N3.png)

Also choose hosted service
![](https://hackmd.io/_uploads/S1k5AZn42.png)

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

![](https://hackmd.io/_uploads/B1kWAa24n.png)

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

![](https://hackmd.io/_uploads/HyjpJ0hEn.png)

And with this, you have initialize your subgraph project.

For a sneak peek of how a working folder should look like, check out [https://github.com/amoweolubusayo/RSVP-composer/tree/main/packages/subgraphs](https://github.com/amoweolubusayo/RSVP-composer/tree/main/packages/subgraphs). 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

```graphql
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



![](https://hackmd.io/_uploads/SyDiZR3E2.png)

![](https://hackmd.io/_uploads/SJnhh-3En.png)

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 

![](https://hackmd.io/_uploads/SkbYm0242.png)


```yaml
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

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

![](https://hackmd.io/_uploads/SyDs_ChV3.png)


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.

```typescript
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

```bash
npm i @graphprotocol/graph-ts
```

```bash
npm i @protofire/subgraph-toolkit
```


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

Run this command

```bash
graph build
```
Your output should look like this

![](https://hackmd.io/_uploads/ryX7F02E3.png)

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

```bash
graph auth --product hosted-service YOUR_KEY
```
Expect this output

![](https://hackmd.io/_uploads/SJoKcA242.png)

Finally, run the deploy command

```bash
graph deploy --product hosted-service amoweolubusayo/rsvp
```

Your output should look like this

![](https://hackmd.io/_uploads/HyjNqA243.png)

Go back to your subgraph dashboard to see changes

![](https://hackmd.io/_uploads/Sy21202N3.png)

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. 