2.8 Interaction and Independence

In the last section, we made our Rock, Paper, Scissors! run until there was a definitive winner. In this section, we won’t be making any changes to the Reach program itself. Instead, we’ll go under the covers of reach run, as well as build a version of our game that is interactive and can be played away from a private developer test network.

In the past, when we’ve run ./reach run, it would create a Docker image just for our Reach program that contained a temporary Node.js package connecting our JavaScript frontend to the Reach standard library and a fresh instance of a private developer test network. Since in this section, we will customize this and build a non-automated version of Rock, Paper, Scissors!, as well as give the option to connect to a real Ethereum network.

We’ll start by running

  $ ./reach scaffold

which will automatically generate the following files for us:

We’re going to leave the first two files unchanged. You can look at them at tut-7/package.json and tut-7/Dockerfile, but the details aren’t especially important. However, we’ll customize the other two files.

First, let’s look at the tut-7/docker-compose.yml file:

 1    version: '3.4'
 2    x-app-base: &app-base
 3      image: reachsh/reach-app-tut-7:latest
 4    services:
 5      ethereum-devnet:
 6        image: reachsh/ethereum-devnet:0.1
 7      algorand-devnet:
 8        image: reachsh/algorand-devnet:0.1
 9        depends_on:
10          - algorand-postgres-db
11        environment:
12          - REACH_DEBUG
13          - POSTGRES_HOST=algorand-postgres-db
14          - POSTGRES_USER=algogrand
15          - POSTGRES_PASSWORD=indexer
16          - POSTGRES_DB=pgdb
17        ports:
18          - 9392
19      algorand-postgres-db:
20        image: postgres:11
21        environment:
22          - POSTGRES_USER=algogrand
23          - POSTGRES_PASSWORD=indexer
24          - POSTGRES_DB=pgdb
25      reach-app-tut-7-ETH-live:
26        <<: *app-base
27        environment:
28          - REACH_DEBUG
29          - REACH_CONNECTOR_MODE=ETH-live
30          - ETH_NODE_URI
31          - ETH_NODE_NETWORK
32      reach-app-tut-7-ETH-test-dockerized-geth: &default-app
33        <<: *app-base
34        depends_on:
35          - ethereum-devnet
36        environment:
37          - REACH_DEBUG
38          - REACH_CONNECTOR_MODE=ETH-test-dockerized-geth
39          - ETH_NODE_URI=http://ethereum-devnet:8545
40      reach-app-tut-7-ETH-test-embedded-ganache:
41        <<: *app-base
42        environment:
43          - REACH_DEBUG
44          - REACH_CONNECTOR_MODE=ETH-test-embedded-ganache
45      reach-app-tut-7-FAKE-test-embedded-mock:
46        <<: *app-base
47        environment:
48          - REACH_DEBUG
49          - REACH_CONNECTOR_MODE=FAKE-test-embedded-mock
50      reach-app-tut-7-ALGO-test-dockerized-algod-local:
51        <<: *app-base
52        environment:
53          - REACH_DEBUG
54          - REACH_CONNECTOR_MODE=ALGO-test-dockerized-algod
55          - ALGO_SERVER=http://host.docker.internal
56          - ALGO_PORT=4180
57          - ALGO_INDEXER_SERVER=http://host.docker.internal
58          - ALGO_INDEXER_PORT=8980
59        extra_hosts:
60          - 'host.docker.internal:172.17.0.1'
61      reach-app-tut-7-ALGO-test-dockerized-algod:
62        <<: *app-base
63        depends_on:
64          - algorand-devnet
65        environment:
66          - REACH_DEBUG
67          - REACH_CONNECTOR_MODE=ALGO-test-dockerized-algod
68          - ALGO_SERVER=http://algorand-devnet
69          - ALGO_PORT=4180
70          - ALGO_INDEXER_SERVER=http://algorand-devnet
71          - ALGO_INDEXER_PORT=8980
72      reach-app-tut-7-: *default-app
73      reach-app-tut-7: *default-app
74      # After this is new!
75      player: &player
76        <<: *default-app
77        stdin_open: true
78      alice: *player
79      bob: *player

With these in place, we can run

  $ docker-compose run WHICH

where WHICH is reach-app-tut-7-ETH-live for a live instance, or alice or bob for a test instance. If we use the live version, then we have to define the environment variable ETH_NODE_URI as the URI of our Ethereum node.

We’ll modify the tut-7/Makefile to have commands to run each of these variants:

..    
29    .PHONY: run-live
30    run-live:
31    	docker-compose run --rm reach-app-tut-7-ETH-live
32    
33    .PHONY: run-alice
34    run-alice:
35    	docker-compose run --rm alice
36    
37    .PHONY: run-bob
38    run-bob:
39    	docker-compose run --rm bob

However, if we try to run either of these, it will do the same thing it always has: create test accounts for each user and simulate a random game. Let’s modify the JavaScript frontend and make them interactive.

We’ll start from scratch and show every line of the program again. You’ll see a lot of similarity between this and the last version, but for completeness, we’ll show every line.

 1    import { loadStdlib } from '@reach-sh/stdlib';
 2    import * as backend from './build/index.main.mjs';
 3    import { ask, yesno, done } from '@reach-sh/stdlib/ask.mjs';
 4    
 5    (async () => {
 6    const stdlib = await loadStdlib();
..    // ...

..    // ...
 7    
 8    const isAlice = await ask(
 9      `Are you Alice?`,
10      yesno
11    );
12    const who = isAlice ? 'Alice' : 'Bob';
..    // ...

..    // ...
13    
14    console.log(`Starting Rock, Paper, Scissors! as ${who}`);
15    
16    let acc = null;
17    const createAcc = await ask(
18      `Would you like to create an account? (only possible on devnet)`,
19      yesno
20    );
21    if (createAcc) {
22      acc = await stdlib.newTestAccount(stdlib.parseCurrency(1000));
23    } else {
24      const secret = await ask(
25        `What is your account secret?`,
26        (x => x)
27      );
28      acc = await stdlib.newAccountFromSecret(secret);
29    }
..    // ...

..    // ...
30    
31    let ctc = null;
32    const deployCtc = await ask(
33      `Do you want to deploy the contract? (y/n)`,
34      yesno
35    );
36    if (deployCtc) {
37      ctc = acc.deploy(backend);
38      const info = await ctc.getInfo();
39      console.log(`The contract is deployed as = ${JSON.stringify(info)}`);
40    } else {
41      const info = await ask(
42        `Please paste the contract information:`,
43        JSON.parse
44      );
45      ctc = acc.attach(backend, info);
46    }
..    // ...

..    // ...
47    
48    const fmt = (x) => stdlib.formatCurrency(x, 4);
49    const getBalance = async () => fmt(await stdlib.balanceOf(acc));
50    
51    const before = await getBalance();
52    console.log(`Your balance is ${before}`);
53    
54    const interact = { ...stdlib.hasRandom };
..    // ...

Next we define a few helper functions and start the participant interaction interface.

..    // ...
55    
56    interact.informTimeout = () => {
57      console.log(`There was a timeout.`);
58      process.exit(1);
59    };
..    // ...

First we define a timeout handler.

..    // ...
60    
61    if (isAlice) {
62      const amt = await ask(
63        `How much do you want to wager?`,
64        stdlib.parseCurrency
65      );
66      interact.wager = amt;
67    } else {
68      interact.acceptWager = async (amt) => {
69        const accepted = await ask(
70          `Do you accept the wager of ${fmt(amt)}?`,
71          yesno
72        );
73        if (accepted) {
74          return;
75        } else {
76          process.exit(0);
77        }
78      };
79    }
..    // ...

Next, we request the wager amount or define the acceptWager method, depending on if we are Alice or not.

..    // ...
80    
81    const HAND = ['Rock', 'Paper', 'Scissors'];
82    const HANDS = {
83      'Rock': 0, 'R': 0, 'r': 0,
84      'Paper': 1, 'P': 1, 'p': 1,
85      'Scissors': 2, 'S': 2, 's': 2,
86    };
87    interact.getHand = async () => {
88      const hand = await ask(`What hand will you play?`, (x) => {
89        const hand = HANDS[x];
90        if ( hand == null ) {
91          throw Error(`Not a valid hand ${hand}`);
92        }
93        return hand;
94      });
95      console.log(`You played ${HAND[hand]}`);
96      return hand;
97    };
..    // ...

Next, we define the shared getHand method.

..    // ...
98    
99    const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
100    interact.seeOutcome = async (outcome) => {
101      console.log(`The outcome is: ${OUTCOME[outcome]}`);
102    };
..    // ...

Finally, the seeOutcome method.

..    // ...
103    
104    const part = isAlice ? backend.Alice : backend.Bob;
105    await part(ctc, interact);
106    
107    const after = await getBalance();
108    console.log(`Your balance is now ${after}`);
109    
110    done();
111    })();

Lastly, we choose the appropriate backend function and await its completion.

We can now run

  $ make build

to rebuilt the images, then

  $ make run-alice

in one terminal in this directory and

  $ make run-bob

in another terminal in this directory.

Here’s an example run:

$ make run-alice

Are you Alice?

y

Starting Rock, Paper, Scissors as Alice

Would you like to create an account? (only possible on devnet)

y

Do you want to deploy the contract? (y/n)

y

The contract is deployed as = {"address":"0xc2a875afbdFb39b1341029A7deceC03750519Db6","creation_block":18,"args":[],"value":{"type":"BigNumber","hex":"0x00"},"creator":"0x2486Cf6C788890885D71667BBCCD1A783131547D"}

Your balance is 999.9999

How much do you want to wager?

10

What hand will you play?

r

You played Rock

The outcome is: Bob wins

Your balance is now 989.9999

and

$ make run-bob

Are you Alice?

n

Starting Rock, Paper, Scissors as Bob

Would you like to create an account? (only possible on devnet)

y

Do you want to deploy the contract? (y/n)

n

Please paste the contract information:

{"address":"0xc2a875afbdFb39b1341029A7deceC03750519Db6","creation_block":18,"args":[],"value":{"type":"BigNumber","hex":"0x00"},"creator":"0x2486Cf6C788890885D71667BBCCD1A783131547D"}

Your balance is 1000

Do you accept the wager of 10?

y

What hand will you play?

p

You played Paper

The outcome is: Bob wins

Your balance is now 1009.9999

Of course, when you run the exact amounts and addresses may be different.

If your version isn’t working, look at the complete versions of tut-7/index.rsh, tut-7/index.mjs, tut-7/package.json, tut-7/Dockerfile, tut-7/docker-compose.yml, and tut-7/Makefile to make sure you copied everything down correctly!

If we were to edit tut-7/docker-compose.yml, and move the &default-app on line 24 to line 51, then instead of running on Ethereum, we’d be able to test and run our application on Algorand.

We may need to also change line 32 of tut-7/index.rsh that defines DEADLINE to be 10 to a higher number, like 30. This is because Algorand does not support an input-enabled developer network that only runs rounds when transactions are present, so it is possible that timeouts will occur unexpectedly. We’ve commonly observed this on machines under heavy CPU load.

Now our implementation of Rock, Paper, Scissors! is finished! We are protected against attacks, timeouts, and draws, and we can run interactively on non-test networks.

In this step, we made a command-line interface for our Reach program. In the next step, we’ll replace this with a Web interface for the same Reach program.

Check your understanding: True or false: Reach helps you build automated tests for your decentralized application, but it doesn’t support building interactive user-interfaces.

Answer:

False; Reach does not impose any constraints on what kind of frontend is attached to your Reach application.