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:
package.json —
A Node.js package file that connects our index.mjs to the Reach standard library. Dockerfile —
A Docker image script that builds our package efficiently and runs it. docker-compose.yml —
A Docker Compose script that connects our Docker image to a fresh instance of the Reach private developer test network. Makefile —
A Makefile that easily rebuilds and runs the Docker image.
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
Lines 2 and 3 define a service for starting our application. Your line 3 will say tut, rather than tut-7, if you’ve stayed in the same directory througout the tutorial.
Lines 5 and 6 define the Reach private developer test network service for Ethereum.
Lines 7 through 24 define the Reach private developer test network service for Algorand.
Lines 25 through 73 define services that allow the application to be run with different networks; including line 24, which defines reach-app-tut-7-ETH-live for connecting to a live network.
We’ll also add lines 73 through 77 to define a player service that is our application with an open standard input, as well as two instances named alice and bob.
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.
Lines 1 and 2 are the same as before: importing the standard library and the backend.
Line 3 is new and imports a helpful library for simple console applications called ask.mjs from the Reach standard library. We’ll see how these three functions are used below.
Lines 7 through 10 ask the question whether they are playing as Alice and expect a "Yes" or "No" answer.
ask
presents a prompt and collects a line of input until its argument does not error.yesno
errors if it is not given "y" or "n".
.. // ...
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 }
.. // ...
Lines 16 through 19 present the user with the choice of creating a test account if they can or inputing a secret to load an existing account.
Line 21 creates the test account as before.
Line 27 loads the existing account.
.. // ...
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 }
.. // ...
Lines 31 through 34 ask if the participant will deploy the contract.
Lines 36 through 38 deploy it and print out public information (
ctc.getInfo
) that can be given to the other player.Lines 40 through 44 request, parse, and process this information.
.. // ...
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.
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 be10
to a higher number, like30
. 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.