On this page:
4.5.1 Problem Analysis
4.5.2 Data Definition
4.5.3 Communication Construction
4.5.4 Assertion Insertion
4.5.5 Interaction Introduction
4.5.6 Deployment Decisions
4.5.7 Discussion and Next Steps
0.1.2

4.5 Workshop: Fear of Missing Out Generalized

In this workshop, we will extend our Fear of Missing Out application with the ability to split the reward between the N most recent Buyers.

In this version, the Funder will have the advantage that, if there are less than N Buyers, the Funder will earn the rewards for every absent Buyer. For example, if the auction is set to have 5 winners, yet only 3 Buyers bid, the first three Buyers will receive 1/5 of the funds each, and the Funder will receive the remaining 2/5 of the funds.

This workshop assumes that you have recently completed Workshop: Fear of Missing Out.

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

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.5.1 Problem Analysis

Our problem analysis is practically the same as the original Fear of Missing Out application, except for one difference:




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



Let’s compare answers for how funds should change ownership in this generalized version:

4.5.2 Data Definition

The data type representation of this program will basically be the same as the regular Fear of Missing Out program. However, instead of tracking the latest Buyer as an Address, we will track the last N Buyers as an Array(Address, N).

You should take the time now to fill out the interaction interface for the participants.




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



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

It is worth noting that Reach does not support arbitrarily sized arrays, so we could not determine NUM_OF_WINNERS at runtime, e.g. from the interaction interface. However, we can still write a program that is generic in the size of the array, then specialize it when we compile.

..    // ...
 4    const NUM_OF_WINNERS = 3;
 5    
 6    const CommonInterface = {
 7    showOutcome: Fun([Array(Address, NUM_OF_WINNERS)], Null),
 8    };
 9    
10    const FunderInterface = {
11    ...CommonInterface,
12    getParams: Fun([], Object({
13      deadline: UInt, // relative deadline
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.5.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. In our original Fear of Missing Out implementation, we outlined the pattern of communication as follows:

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

This outline will need to be updated for our generalized version. 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 the winners (last N Buyers)
// 3. Divide the balance evenly amongst the winners.
// 4. Transfer the reward to each winner.

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 initialWinners = Array.replicate(NUM_OF_WINNERS, Funder);

const [ keepGoing, winners, ticketsSold ] =
  // 2. While the deadline has yet to be reached:
  parallel_reduce([ true, initialWinners, 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;
        // 2b. Keep track of the winners (last N Buyers)
        const idx = ticketsSold % NUM_OF_WINNERS;
        const newWinners =
          Array.set(winners, idx, buyer);
        return [ true, newWinners, ticketsSold + 1 ]; })
    .timeout(deadline, () => {
      race(Buyer, Funder).publish();
      return [ false, winners, ticketsSold ]; });

// 3. Divide the balance evenly amongst the winners.
transfer(balance() % NUM_OF_WINNERS).to(Funder);
const reward = balance() / NUM_OF_WINNERS;

// 4. Transfer the reward to each winner.
winners.forEach(winner =>
  transfer(reward).to(winner));

commit();

Extending this program to track an array of Addresses, as opposed to a single Address is fairly straightforward. We maintain an array of size NUM_OF_WINNERS and implement a ring buffer to keep it up to date with the most recent N winners, as demonstrated in step 2b.

Another aspect of this code worth highlighting is step 3. We transfer balance() % NUM_OF_WINNERS to the winner because the total balance may not be evenly divisible by the number of winners.

For example, if the ticket price is 4 ETH and there are 10 tickets purchased by Buyers, then the total balance will be 40 ETH. However, if the application is set to select 3 winners, then 40 cannot be evenly distributed to 3 participants. So, we will transfer 1 ETH to the Funder, and split the remaining 39 ETH between the 3 Buyers.

4.5.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 parallel_reduce invariant which states that the balance must be equal to the number of tickets sold multiplied by the ticket price.

4.5.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    
 3    // FOMO Workshop generalized to last N winners
 4    const NUM_OF_WINNERS = 3;
 5    
 6    const CommonInterface = {
 7      showOutcome: Fun([Array(Address, NUM_OF_WINNERS)], Null),
 8    };
 9    
10    const FunderInterface = {
11      ...CommonInterface,
12      getParams: Fun([], Object({
13        deadline: UInt, // relative deadline
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), ParticipantClass('Buyer', BuyerInterface),
28      ],
29      (Funder, Buyer) => {
30        const showOutcome = (winners) =>
31          each([Funder, Buyer], () => interact.showOutcome(winners));
32    
33        Funder.only(() => {
34          const { ticketPrice, deadline } = declassify(interact.getParams()); });
35        Funder.publish(ticketPrice, deadline);
36    
37        const initialWinners = Array.replicate(NUM_OF_WINNERS, Funder);
38    
39        // Until deadline, allow buyers to buy ticket
40        const [ keepGoing, winners, ticketsSold ] =
41          parallel_reduce([ true, initialWinners, 0 ])
42            .invariant(balance() == ticketsSold * ticketPrice)
43            .while(keepGoing)
44            .case(
45              Buyer,
46              (() => ({
47                when: declassify(interact.shouldBuyTicket(ticketPrice)) })),
48              (() => ticketPrice),
49              () => {
50                const buyer = this;
51                Buyer.only(() => interact.showPurchase(buyer));
52                const idx = ticketsSold % NUM_OF_WINNERS;
53                const newWinners =
54                  Array.set(winners, idx, buyer);
55                return [ true, newWinners, ticketsSold + 1 ]; })
56            .timeout(deadline, () => {
57              race(Buyer, Funder).publish();
58              return [ false, winners, ticketsSold ]; });
59    
60        transfer(balance() % NUM_OF_WINNERS).to(Funder);
61        const reward = balance() / NUM_OF_WINNERS;
62    
63        winners.forEach(winner =>
64          transfer(reward).to(winner));
65    
66        commit();
67        showOutcome(winners);
68      });

4.5.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(3),
22        deadline: 8,
23      };
24    
25      const resultText = (outcome, addr) =>
26        outcome.includes(addr) ? 'won' : 'lost';
27    
28      await Promise.all([
29        backend.Funder(ctcFunder, {
30          showOutcome: (outcome) =>
31            console.log(`Funder saw they ${resultText(outcome, accFunder.networkAccount.address)}`),
32          getParams: () => funderParams,
33        }),
34      ].concat(
35        accBuyerArray.map((accBuyer, i) => {
36          const ctcBuyer = accBuyer.attach(backend, ctcInfo);
37          const Who = `Buyer #${i}`;
38          return backend.Buyer(ctcBuyer, {
39            showOutcome: (outcome) =>
40              console.log(`${Who} saw they ${resultText(outcome, accBuyer.networkAccount.address)}`),
41            shouldBuyTicket : () => Math.random() < 0.5,
42            showPurchase: (addr) => {
43              if (stdlib.addressEq(addr, accBuyer)) {
44                console.log(`${Who} bought a ticket.`);
45              }
46            }
47          });
48        })
49      ));
50    })();

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

$ ../reach run

Buyer #6 bought a ticket.

Buyer #3 bought a ticket.

...

Buyer #4 bought a ticket.

Buyer #1 bought a ticket.

Buyer #3 bought a ticket.

Buyer #8 bought a ticket.

Buyer #6 bought a ticket.

Funder saw they lost

Buyer #1 saw they lost

Buyer #5 saw they lost

Buyer #9 saw they lost

Buyer #0 saw they lost

Buyer #8 saw they won

Buyer #7 saw they lost

Buyer #2 saw they lost

Buyer #6 saw they won

Buyer #3 saw they won

Buyer #4 saw they lost

4.5.7 Discussion and Next Steps

Great job!

You’ve now implemented a generalized Fear of Missing Out game. You can try extending this application with additional features such as:

If you found this workshop rewarding, please let us know on the Discord community!