A Workshop on Building an RSVP DApp in Reach

A Workshop on Building an RSVP DApp in Reach

This is a top down walkthrough of a Decentralized Reservation app. In this workshop, we'll design an application that allows an event host to create an event, set the number of tickets available, set the ticket fee to be paid by attendees in order to reserve a seat at the event, and when the attendees check in on the event date, the fee gets refunded to them, and if they do not check in, the fee is sent to the Host. It is like a sort of commitment fee to make people come for the events they made a reservation for. This workshop uses the API participant class to represent Attendees, which allows us to handle multiple participants in a generic way.

We assume that you'll go through this workshop in a directory named

~/reach/workshop-rsvp

You can create the folder and navigate into it using the command

$ mkdir -p ~/reach/workshop-rsvp && cd ~/reach/workshop-rsvp

And ensure you have a copy of Reach installed in ~/reach. The command below confirms this as it will return the reach version you are running on.

$ ../reach version

You should start off by initializing your Reach program

$ ../reach init

Problem Analysis

The first thing to do is to analyze the problem and determine the information relevant to the problem. When writing decentralized applications in Reach, problem analysis includes an analysis of the set of participants involved in a program.

You should write the answer to the following questions in your Reach program (index.rsh file) using a comment.

/* Remember comments are written like this. */
  • Who are the participants in this application?
  • What information do they know at the start of the program?
  • What information are they going to discover and use in the program?
  • What funds change ownership during the application and how?

Write down the problem analysis of this program as a comment.

Let's see how your answers compare to our answers

  • This program involves two parties: the event Host that creates an event, and the Attendees that make reservations for the event by purchasing the event tickets.
  • The Host knows the event details and the ticket price at the start of the application.
  • The Attendees do not know about the event details or ticket price at the beginning of the application.
  • The Host gets notified when an attendee makes a reservation and if the attendee checks in or not during the program execution.
  • The Attendees learn of the event details and ticket price during the program execution.
  • Attendees pay the required ticket fee to reserve a seat and when they check-in on event date, the fee gets refunded.

It's okay if your answers are different than ours. Problem analysis isn't a rigid process and is more of critical and creative thinking while writing down your thought process.

Problem analysis helps us understand our application better and the expectations we want from it. Reach is awesome because it automatically discovers problems you may not have realized your program had as well.

Data Definition

After we have analyzed the problem, we need to decide how we will represent the information in the program that is the data types.

  • What data type will represent the event details (Title, Location, Description, Number of Tickets, Ticket Price, Organizer, Date) set by the Funder?
  • What data type will represent the Attendee's decision to purchase a ticket ( RSVP) and check in into the event?

Refer to Types for a reminder of what data types are available in Reach.

After deciding the data types to use, we need to determine how the program will obtain this information. We need to outline the participant interact interface for each participant.

  • What participant interact interface will the Host use?
  • What participant interact interface will the Attendees use?

A quick note to make is whenever a participant starts off with some knowledge, that will be a field in the interact object. If they learn something, then it will be an argument to a function. If they provide something later, then it will be the result of a function.

Below are the data definitions for the RSVP program and it is done in your index.rsh file.

Quick check with you. Hope you have something similar to this.

/examples/workshop-rsvp/index.rsh
'reach 0.1';


// The event information structure (object)
const Details = Object({
  title: Bytes(64),
  location: Bytes(64),
  fee: UInt,
  tickets: UInt,
  organizer: Bytes(64),
  date: Bytes(64),
  description: Bytes(64),
})

export const main = Reach.App(() => {
  setOptions({ untrustworthyMaps: true });

  const Host = Participant('Host', {
    // The Host interact interface for managing events
    createEvent: Details,
    seeRSVP: Fun([Address], Null),
    confirmGuest: Fun([Address], Null),
    manageFunds: Fun([], Null),
  });

  const Attendee = API('Attendee', {
    // The Attendee interact interface for events
    rsvpForEvent: Fun([Bytes(64), Bytes(64), UInt, UInt, Bytes(64), Bytes(64), Bytes(64)], Null),  
    checkIn: Fun([], Null)
  });

  // View for showing the event details in the Frontend
  const Info = View('Info', {
    details: Details,
  });

We've represented the event details as an object whose properties are the event fields with their respective data types as values. We know that the title, location, organizer, date and description will be strings and strings are represented as Bytes in Reach while the fee and number of tickets are of the integers and are represented by the datatype UInt since they are unsigned.

Next is the participant interact interface for both the Host and Attendees. The participant interact interface is filled mainly with actions to be performed by the Host and Attendees in addition to some handy logging functions to see the actions being performed.

Communication Construction

This entails defining the pattern of communication between the participants outside and within the consensus network, how transfer of funds is made, etc. You could see it like who initiates the application? Who responds next? Is there any part of the program that occurs over and over again? Is there any point where funds are being exchanged?, etc. Let's write down this communication pattern as comments in our program.

You should do this now, in your Reach program (index.rsh).

1. // The Host publishes the details of the event which has the event fee
2. // The Attendee pays the event fee and registers for the event
3. // The consensus ensures it's the right fee
4. // The Host see that the particular Attendee has made a reservation
5. // The Attendee checks into the event on event date
6. // The Host sees and confirms the check in
7. // The event fee gets refunded back to the Attendee
8. // If the Attendee does not check in on event date, the fee is sent to the Host. 
9. // The program continues to allow Attendees make reservations until the number of tickets set runs out.

We then convert this pattern into actual program code using publish, pay, and commit. We are using the API Participant because we want multiple Attendees to be able to make reservations. The parallelReduce handles the main logic of our program.

The body of your application should look something like

 // Host creating, publishing the event details and deploying the contract
  Host.only(() => {
    const details = declassify(interact.createEvent);
  })
  Host.publish(details);
  const {title, location, fee, tickets, organizer, date, description} = details;
  Info.details.set(details);
  const Guests = new Map(Bool);

  // Attendee RSVPing for the event using a parallel reduce for the process
  const [ numTickets] = parallelReduce([ tickets])
  .invariant(balance() >= 0)
  .while(numTickets > 0)
  .api_(Attendee.rsvpForEvent, (titl, locate, fees, tick, organize, time, descrip) => {
    check(fees == fee, "Fee can't be zero");
    return [fees, (notify) => {
      Guests[this] = true;
      notify(null);
      const who = this; 
      Host.interact.seeRSVP(who); 
      return [ numTickets ];
    }];
  })
  .api_(Attendee.checkIn, () => {
    return [ (notify) => {
      notify(null);
      const who = this; 
      Host.interact.confirmGuest(who); 
      transfer(balance()).to(who);
      return [ numTickets - 1 ];
    } ];
  });

  transfer(balance()).to(Host);

  commit();

  exit();
});

We can now move on to the next part of designing a decentralized application: verification.

Assertion Insertion

Assertion is a way of encoding the behavior of the program that helps us know what should happen next in the program based on past occurences and what is true at every point in the program. directly into the text of the program in a way that will be validated by the Reach verification engine. It's about the assumptions and logical properties which must be true about the program.

For our application, there are no assertion statements as the logical properties are those automatically included in all Reach programs, such as ensuring that the funds are used linearly and no token is left over in the contract at the end of the program. A notable property of interest is the parallelReduce invariant which states that the balance must be equal to zero initially and as reservations are being made, the balance in the contract must be equal to the number of tickets sold multiplied by the event fee.

Interaction Introduction

We have been writing the smart contract (which is the communication and consensus part of a decentralized application) but users need a user interface or frontend to interact with which completes the application. We insert calls into our code to send data to and get data from the frontend via the participant interact interfaces that we defined earlier.

And here's our whole program for single participants that is a Host and an Attendee

'reach 0.1';

export const main = Reach.App(() => {
  const Admin = Participant('Admin', {
    // Specify Alice's interact interface here
    createEvent: Fun([], Object({
      title: Bytes(64),
      fee: UInt,
      location: Bytes(64),
      description: Bytes(64),
      tickets: UInt,
      organizer: Bytes(64),
    })),
    seeRSVP: Fun([Address], Null),
    confirmGuest: Fun([Address], Null),
    manageFunds: Fun([], Null),
  });
  const Attendee = Participant('Attendee', {
    // Specify Bob's interact interface here
    rsvpForEvent: Fun([UInt, Bytes(64)], Null),  
    checkIn: Fun([], Null)
  });
  init();

  Admin.only(() => {
    const { title, fee, location, description, tickets, organizer } = declassify(interact.createEvent());
  })
  // The first one to publish deploys the contract
  Admin.publish(title, fee, location, description, tickets, organizer)
    // .pay(fees);
  commit();


  // The second one to publish always attaches
  Attendee.only(() => {
    interact.rsvpForEvent(fee, title);
  })
  Attendee.publish()
    .pay(fee);
  commit()

  Admin.interact.seeRSVP(Attendee);

  Attendee.only(() => {
    const checkIn = declassify(interact.checkIn());
  })
  Attendee.publish(checkIn);
  commit();

  Admin.only(() => {
    const manageFunds = declassify(interact.manageFunds());
    const confirmGuest = declassify(interact.confirmGuest(Attendee));
  })
  Admin.publish(manageFunds, confirmGuest);
  transfer(balance()).to(Attendee);

  commit();

  exit();
});

For multiple attendees, here's the full implementation

1.  'reach 0.1';
2.
3.  // The event information structure (object)
4.   const Details = Object({
5.    title: Bytes(64),
6.    location: Bytes(64),
7.    fee: UInt,
8.    tickets: UInt,
9.    organizer: Bytes(64),
10.  date: Bytes(64),
11.     description: Bytes(64),
13.       })
14.
15.  export const main = Reach.App(() => {
16.   setOptions({ untrustworthyMaps: true });
17.
18.  const Host = Participant('Host', {
19.    // The Host interact interface for managing events
20.    createEvent: Details,
21.    seeRSVP: Fun([Address], Null),
22.    confirmGuest: Fun([Address], Null),
23.   manageFunds: Fun([], Null),
24.  });
25.
26.  const Attendee = API('Attendee', {
27.    // The Attendee interact interface for events
28.    rsvpForEvent: Fun([Bytes(64), Bytes(64), UInt, UInt, Bytes(64), Bytes(64), Bytes(64)], Null),  
29.    checkIn: Fun([], Null)
30.  });
31.
32.  // View for showing the event details in the Frontend
33.  const Info = View('Info', {
34.    details: Details,
35.  });
36.  
37.  init();
38.
39.  // Host creating, publishing the event details and deploying the contract
40.  Host.only(() => {
41.    const details = declassify(interact.createEvent);
42.  })
43.  Host.publish(details);
44.  const {title, location, fee, tickets, organizer, date, description} = details;
45.  Info.details.set(details);
46.  const Guests = new Map(Bool);
47.
48.  // Attendee RSVPing for the event using a parallel reduce for the process
49.  const [ numTickets] = parallelReduce([ tickets])
50.  .invariant(balance() >= 0)
51.  .while(numTickets > 0)
52.  .api_(Attendee.rsvpForEvent, (titl, locate, fees, tick, organize, time, descrip) => {
53.    check(fees == fee, "Fee can't be zero");
54.    return [fees, (notify) => {
55.      Guests[this] = true;
56.      notify(null);
57.      const who = this; 
58.      Host.interact.seeRSVP(who); 
59.      return [ numTickets ];
60.    }];
61.  })
62.  .api_(Attendee.checkIn, () => {
63.    return [ (notify) => {
64.      notify(null);
65.      const who = this; 
66.      Host.interact.confirmGuest(who); 
67.      transfer(balance()).to(who);
68.      return [ numTickets - 1 ];
69.    } ];
70.  });
71.  
72.  transfer(balance()).to(Host);
73.
74.  commit();
75.
76.  exit();
77.  });

We can see we used the interact function on lines 41, 58, and 66 to get the details of the event created by the host from the frontend,

We can now run

$ ../reach compile

and we'll get a verification message that all our theorems passed.

work.png

Great job so far! But our application needs to run so we can interact with the smart contract.

Deployment Decisions

Finally, we need to decide how we're going to deploy this program and use it in production. For now, we will test our program using a completely automated and interactive test deployment in our terminal and later you can build a more creative frontend for our reservation dapp. We will create test accounts for the Host and any number of Attendees. The decision to make a reservation will be based on generating a random boolean.

Here's the JavaScript frontend we wrote in the index.mjs file.

workshop-rsvp/index.mjs
import {loadStdlib, ask } from '@reach-sh/stdlib';
import * as backend from './build/index.main.mjs';
const stdlib = loadStdlib(process.env);

const isAdmin = await ask.ask(
  'Are you an Admin',
  ask.yesno
)

const who = isAdmin ? 'Admin' : 'Attendee';

console.log(`Welcome to the RSVP App as ${who}`);

let acc = null;
const createAcc = await ask.ask(
    `Would you like to create an account? (only possible on devnet)`,
    ask.yesno
);

if (createAcc) {
    acc = await stdlib.newTestAccount(stdlib.parseCurrency(1000));
} else {
    const secret = await ask.ask(
        'Enter your secret key',
        (x => x)
    );
    acc = await stdlib.newAccountFromSecret(secret);
}

let ctc = null;
if (isAdmin) {
    ctc = acc.contract(backend);
    ctc.getInfo().then((info) => {
        console.log(`Your contract is deployed as = ${JSON.stringify(info)}`);
    });
} else {
    const info = await ask.ask(
        'Please paste the contract information: ',
        JSON.parse
    );
    ctc = acc.contract(backend, info);
}

const fmt = (x) => stdlib.formatCurrency(x, 4);
const getBalance = async () => fmt(await stdlib.balanceOf(acc));

const before = await getBalance();
console.log(`Your balance before is ${before} ${stdlib.standardUnit}`);

const interact = { ...stdlib.hasRandom };

let eventDetails = null;

if (isAdmin) {

  console.log('Creating the Event...');

  const title = await ask.ask(
    'Enter the event name:',
    (x => x)
  );
  const fee = await ask.ask(
    'How much would you like to charge for the event?',
    stdlib.parseCurrency,
  );
  const location = await ask.ask(
    'Where is the event?',
    (x => x)
  );
  const description = await ask.ask(
    'What is the event about?',
    (x => x)
  );
  const tickets = await ask.ask(
    'How many tickets would you like to sell?',
    (x => x)
  );
  const organizer = await ask.ask(
    'Who is the event organizer?',
    (x => x)
  );
  eventDetails = { title, fee, location, description, tickets, organizer };

  interact.deadline = { ETH: 100, ALGO: 100, CFX: 1000 } [stdlib.connector];    
};

console.log('Starting backends...');

interact.createEvent = () => {
  console.log(`The event details is sent to the contract:`, eventDetails);
  return eventDetails;
},

interact.seeRSVP = (who) => {
  console.log(`${stdlib.formatAddress(who)} made a reservation for the event.`);
},

interact.confirmGuest = (who) => {
  console.log(`${stdlib.formatAddress(who)} has checked in.`);
},


interact.manageFunds = () => {
  console.log(`The funds are managed`);
},

interact.rsvpForEvent = async (fee, title) => {
  console.log(`You paid ${fmt(fee)} ${stdlib.standardUnit} for the event`);
  console.log(`You have RSVPed for the event: ${title} which costs ${fmt(fee)} ${stdlib.standardUnit}`);
}

interact.checkIn = async (who) => {
  const check = await ask.ask(
    'Would you like to check in?',
    ask.yesno
  );
  if (check) {    
    const now = await getBalance();
    console.log(`Your balance after RSVP is ${now} ${stdlib.standardUnit}`);
   console.log(`You have checked in. You should get your money back.`);
  } else {
    console.log(`You have not checked in.`);
  }
}

// implement Attendee's interact object here

const part = isAdmin ? ctc.p.Admin : ctc.p.Attendee;
await part(interact);

const after = await getBalance();
console.log(`Your balance is now ${after}`);

console.log('Goodbye, Admin and Attendee!');
ask.done();

With this testing frontend in place, we can run the program using the command below.

$ ../reach run

and see an example execution:

Let's see what it looks like when we run the program:

Host.png

attendee.png

Discussion

Voila!!!. You implemented a simple RSVP DApp in Reach with a little bit of help. If you found this workshop rewarding, please let us know on the Discord community!

If you'd like to make this application a little more interesting, you can code out the frontend for multiple attendees as the test frontend code we showed above is for a single attendee. If you want to extend this program, you can make the host pay a platform fee before being allowed to create an event as well.