On this page:
4.4.1 Problem Analysis
4.4.2 Data Definition
4.4.3 Communication Construction
4.4.4 Assertion Insertion
4.4.5 Interaction Introduction
4.4.6 Deployment Decisions
4.4.7 Discussion and Next Steps
0.1.2

4.4 Workshop: Fear of Missing Out

Chris Nevers <cnevers@reach.sh>

In this workshop, we’ll design an application that allows a Funder to create an auction where participants may purchase tickets. The Funder sets a ticket price and a relative deadline. When a Buyer purchases a ticket, the deadline is reset. Whomever is the last person to buy a ticket—when the deadline finally hits—wins the entire balance. This program is based off of the crypto game, FOMO3DGame.

This workshop utilizes participant classes to represent Buyers, which allows us to handle multiple participants in a generic way.

This workshop is independent of all others.

We assume that you’ll go through this workshop in a directory named ~/reach/workshop-fomo:
  $ mkdir -p ~/reach/workshop-fomo && cd ~/reach/workshop-fomo

And that you have a copy of Reach installed in ~/reach so you can write
  $ ../reach version

And it will run Reach.

You should start off by initializing your Reach program:
  $ ../reach init

4.4.1 Problem Analysis

First, we should think over the details of the application and answer some questions to help reason about the implementation of the program.

You should write the answer to the following questions in your Reach program (index.rsh) using a comment. /* Remember comments are written like this. */




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



Let’s see how your answers compare to ours:

It’s okay if some of your answers differ from ours!

4.4.2 Data Definition

After problem analysis, we need to decide how we will represent the information in the program:
  • What data type will represent the deadline set by the Funder?

  • What data type will represent the ticket price set by the Funder?

  • What data type will represent the Buyer’s decision to purchase a ticket?

Now that we’ve decided what data types to use, we need to determine how the programs will obtain this information. We need to outline the participant interact interface for each participant.

Revisit the problem analysis section when completing this section. 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.

You should write your answers in your Reach file (index.rsh) as the participant interact interface for each of the participants.




Stop!
Write down the data definitions for this program as definitions.



Let’s compare your answers with ours:

Our participant interact interface, with the addition of some handy logging functions, looks like this so far:

..    // ...
 4    const CommonInterface = {
 5    // Show the address of winner
 6    showOutcome: Fun([Address], Null),
 7    };
 8    
 9    const FunderInterface = {
10    ...CommonInterface,
11    getParams: Fun([], Object({
12      // relative deadline
13      deadline: UInt,
14      ticketPrice: UInt,
15    })),
16    };
17    
18    const BuyerInterface = {
19    ...CommonInterface,
20    shouldBuyTicket: Fun([UInt], Bool),
21    showPurchase: Fun([Address], Null),
22    };
..    // ...

At this point, you can modify your JavaScript file (index.mjs) to contain defintions of these values, although you may want to use a placeholders for the actual value. When you’re writing a Reach program, especially in the early phases, you should have these two files open side-by-side and update them in tandem as you’re deciding the participant interact interface.

4.4.3 Communication Construction

A fundamental aspect of a decentralized application is the pattern of communication and transfer among the participants. We should write down this structure as comments in our program to serve as an outline and guide us in implementation. For example, for the tutorial version of Rock, Paper, Scissors!, we might write:

// Alice publishes the wager and pays it
// Bob accepts the wager and pays it
// While there's a draw
//  Alice publishes her hand secretly
//  Bob publishes his hand publicly
//  Alice reveals her hand
//  The consensus ensures it's the same hand as before
// The consensus pays out the wager

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




Stop!
Write down the communication pattern for this program as comments.



Here’s what we wrote for our outline:

// 1. The Funder publishes the ticket price and deadline
// 2. While the deadline has yet to be reached:
//     2a. Allow a Buyer to purchase a ticket
//     2b. Keep track of last Buyer
// 3. Transfer the balance to the last person who bought a ticket

Now, this outline needs to be converted to a real program.




Stop!
Write down the communication pattern for this program as code.



The body of your application should look something like this:

// 1. The Funder publishes the ticket price and deadline
Funder.publish(ticketPrice, deadline);

const [ keepGoing, winner, ticketsSold ] =
  // 2. While the deadline has yet to be reached
  parallelReduce([ true, Funder, 0 ])
    .invariant(balance() == ticketsSold * ticketPrice)
    .while(keepGoing)
    .case(
      Buyer,
      // 2a. Allow a Buyer to purchase a ticket
      (() => ({
        when: declassify(interact.shouldBuyTicket(ticketPrice)),
      })),
      (() => ticketPrice),
      (() => {
        const buyer = this;
        Buyer.only(() => interact.showPurchase(buyer));
        // 2b. Keep track of last Buyer
        return [ true, buyer, ticketsSold + 1 ];
      })
    )
    .timeout(deadline, () => {
      race(Buyer, Funder).publish();
      return [ false, winner, ticketsSold ]; });

// 3. Transfer the balance to the last person who bought a ticket
transfer(balance()).to(winner);
commit();

We use parallelReduce to allow Buyers to purchase tickets until the deadline passes and accumulate the current winner. We maintain the invariant that the balance must be equal to the number of tickets sold multiplied by the ticket price.

4.4.4 Assertion Insertion

This program doesn’t have many interesting properties to prove as assertions, beyond the token linearity property. The only property of interest is the parallelReduce invariant which states that the balance must be equal to the number of tickets sold multiplied by the ticket price.

4.4.5 Interaction Introduction

Next, we need to insert the appropriate calls to interact. In this case, our program is very simple and we expect you’ll do a great job without further discussion.




Stop!
Insert interact calls to the frontend into the program.



Let’s look at our whole program now:

 1    'reach 0.1';
 2    'use strict';
 3    
 4    const CommonInterface = {
 5      // Show the address of winner
 6      showOutcome: Fun([Address], Null),
 7    };
 8    
 9    const FunderInterface = {
10      ...CommonInterface,
11      getParams: Fun([], Object({
12        // relative deadline
13        deadline: UInt,
14        ticketPrice: UInt,
15      })),
16    };
17    
18    const BuyerInterface = {
19      ...CommonInterface,
20      shouldBuyTicket: Fun([UInt], Bool),
21      showPurchase: Fun([Address], Null),
22    };
23    
24    export const main = Reach.App(
25      { },
26      [
27        Participant('Funder', FunderInterface),
28        ParticipantClass('Buyer', BuyerInterface),
29      ],
30      (Funder, Buyer) => {
31    
32        // Helper to display results to everyone
33        const showOutcome = (who) =>
34          each([Funder, Buyer], () => {
35            interact.showOutcome(who); });
36    
37        // Have the funder publish the ticket price and deadline
38        Funder.only(() => {
39          const { ticketPrice, deadline } =
40            declassify(interact.getParams());
41        });
42        Funder.publish(ticketPrice, deadline);
43    
44        // Until timeout, allow buyers to buy ticket
45        const [ keepGoing, winner, ticketsSold ] =
46          parallelReduce([ true, Funder, 0 ])
47            .invariant(balance() == ticketsSold * ticketPrice)
48            .while(keepGoing)
49            .case(
50              Buyer,
51              (() => ({
52                when: declassify(interact.shouldBuyTicket(ticketPrice)),
53              })),
54              ((_) => ticketPrice),
55              ((_) => {
56                const buyer = this;
57                Buyer.only(() => interact.showPurchase(buyer));
58                return [ true, buyer, ticketsSold + 1 ];
59              })
60            )
61            .timeout(deadline, () => {
62              Anybody.publish();
63              return [ false, winner, ticketsSold ]; });
64    
65        // Whoever buys last wins and receives balance
66        transfer(balance()).to(winner);
67        commit();
68        showOutcome(winner);
69      });

4.4.6 Deployment Decisions

Next, it is time to test our program. As usual, we’ll present a completely automated test deployment, rather than an interactive one.

The program is fairly straightfoward to test. We just create test accounts for the Funder and any number of Buyers. The decision to purchase a ticket by a Buyer will rely simply on generating a random boolean.




Stop!
Decide how you will deploy and use this application.



Here’s the JavaScript frontend we wrote:

  1    import {loadStdlib} from '@reach-sh/stdlib';
  2    import * as backend from './build/index.main.mjs';
  3    
  4    const numOfBuyers = 10;
  5    
  6    (async () => {
  7      const stdlib = await loadStdlib();
  8      const startingBalance = stdlib.parseCurrency(100);
  9    
 10      const accFunder = await stdlib.newTestAccount(startingBalance);
 11      const accBuyerArray = await Promise.all(
 12        Array.from({ length: numOfBuyers }, () =>
 13          stdlib.newTestAccount(startingBalance)
 14        )
 15      );
 16    
 17      const ctcFunder = accFunder.deploy(backend);
 18      const ctcInfo   = ctcFunder.getInfo();
 19    
 20      const funderParams = {
 21        ticketPrice: stdlib.parseCurrency(5),
 22        deadline: 5,
 23      };
 24    
 25      await Promise.all([
 26        backend.Funder(ctcFunder, {
 27          showOutcome: (addr) => console.log(`Funder saw ${stdlib.formatAddress(addr)} won.`),
 28          getParams: () => funderParams,
 29        }),
 30      ].concat(
 31        accBuyerArray.map((accBuyer, i) => {
 32          const ctcBuyer = accBuyer.attach(backend, ctcInfo);
 33          return backend.Buyer(ctcBuyer, {
 34            showOutcome: (outcome) => {
 35              console.log(`Buyer ${i} saw they ${stdlib.addressEq(outcome, accBuyer) ? 'won' : 'lost'}.`);
 36            },
 37            shouldBuyTicket : () => Math.random() < 0.5,
 38            showPurchase: (addr) => {
 39              if (stdlib.addressEq(addr, accBuyer)) {
 40                console.log(`Buyer ${i} bought a ticket.`);
 41              }
 42            }
 43          });
 44        })
 45      ));
 46    
 47    })();

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

$ ../reach run

Buyer 0 bought a ticket.

Buyer 3 bought a ticket.

...

Buyer 5 bought a ticket.

Buyer 9 bought a ticket.

Buyer 1 bought a ticket.

Funder saw 0x94CAC1b24C1f7b0EBAD4A51797dE5d59A39910C4 won.

Buyer 5 saw they lost.

Buyer 1 saw they won.

Buyer 6 saw they lost.

Buyer 0 saw they lost.

Buyer 4 saw they lost.

Buyer 8 saw they lost.

Buyer 2 saw they lost.

Buyer 7 saw they lost.

Buyer 3 saw they lost.

Buyer 9 saw they lost.

4.4.7 Discussion and Next Steps

Great job!

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, maybe you’d like to extend this program to make the last N buyers split the winnings. Check out Fear of Missing Out Generalized for our solution!