2.8 交互及自主运行

937 阅读6分钟

原文链接

在上面实现的程序中,我们的石头剪刀布会一直运行直到确定最终的赢家为止。在本节中,我们不会改变Reach程序本身。反之,我们将在 reach run 之上建立一个可交互的版本,这个版本将能够在私有开发者测试网络之外(真正以太坊网络)运行。


之前,当我们运行 ./reach run时,它会为我们的 reach 程序创建一个 Docker 映像,其中包含一个临时的 Node.js 包,将我们的 JavaScript 前端连接到 reach 标准库和一个私有开发者测试网络的新实例。在本节中,我们将对其进行定制,并构建一个非自动化版本的剪刀石头布,并提供连接到真正以太坊网络的选项。
首先我们运行
$ ./reach scaffold
它会自动为我们生成以下文件:

  • package.json 一个 Node.js 的包文件,连接 index.mjs 到 Reach 标准库
  • Dockerfile 一个 Docker 镜像脚本,高效地建立并运行package包程序
  • docker-compose.yml 一个 Docker Compose 脚本,链接 Docker 镜像到Reach私有开发人员测试网络的一个新实例。
  • Makefile 一个 Makefile 文件,重新编译并运行 Docker 镜像。

我们将保留前两个文件不变。你可以在tut-8/package.json 和 tut-8/Dockerfile中看到它们。 我们将自定义其他两个文件。
首先,让我们看看 tut-8/docker-compose.yml文件:
 tut-8/docker-compose.yml

 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
  • 第 2 行和第 3 行定义了启动应用程序的服务。如果您在教程中一直停留在同一个目录中,那么第3行将显示tut,而不是 tut-7 。
  • 第 5 行和第 6 行定义了以太坊的 Reach 私有开发者测试网络服务。
  • 第 7 至 24 行定义了为Algorand提供的 Reach private developer 测试网络服务。
  • 第 25 到 73 行定义了允许应用程序在不同网络上运行的服务;包括第 25 行,它定义了 reach-app-tut-8-ETH-live 连接到一个现有网络。
  • 我们还添加了第 73 行到第 77 行来定义一个玩家,它是我们的应用程序,具有开放的标准输入,以及两个名为 alice 和 bob 的实例。

有了这些,我们就可以运行
$ docker-compose run WHICH
其中 WHICH 在 live 实例中是 reach-app-tut-8-ETH-live ,而在测试实例中是 Alice 或者 Bob 。如果我们使用live版本,那么我们必须定义环境变量 ETH_NODE_URI 作为我们以太坊节点的 URI 。

我们将修改 tut-8/Makefile ,让程序可以运行:
 tut-8/Makefile 

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

然而,如果我们尝试运行其中任何一个,它做的事情都一样 : 为每个用户创建测试帐户,并随机模拟游戏。接着我们来修改JavaScript前端,使它们具有交互性。


我们将从头开始,再次展示程序的每一行。您会发现这个版本与先前的版本非常相似,但是为了完整起见,我们会显示每一行。
tut-8/index.mjs

 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();
..    // ...
  • 第 1 行和第 2 行和之前一样 : 导入标准库和后端。
  • 第 3 行是新加入的,为简单的控制台应用程序导入了一个来自 Reach 标准库名为 ask.mjs 的库。下面我们将看到如何使用这三个函数。

tut-8/index.mjs

..    // ...
 7    
 8    const isAlice = await ask(
 9      `Are you Alice?`,
10      yesno
11    );
12    const who = isAlice ? 'Alice' : 'Bob';
..    // ...
  • 第 8 行到第 11 行,问用户是否以 Alice 的身份参与,期望得到“是”或“不是”的回答。ask 会显示一个提示并收集一行输入,直到得到的参数没有出错。出现 yes/no , error 就表示没收到 'y' 或 'n'。

tut-8/index.mjs

..    // ...
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    }
..    // ...
  • 第 16 行到第 19 行,用户可以选择创建一个测试帐户,或者输入一个密码来导入一个现有的帐户。
  • 第 21 行像之前一样创建测试帐户。
  • 第 27 行导入现有帐户。

tut-8/index.mjs

..    // ...
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    }
..    // ...
  • 第 31 至 34 行询问参与者是否要部署合约。
  • 第 36 到 38 行部署它并打印出可以给其他玩家的公共信息(ctc.getInfo)。
  • 第 40 行到第 44 行请求、解析和处理这个信息。

tut-8/index.mjs

..    // ...
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 };
..    // ...

接下来,我们定义几个辅助函数并启动 participant 交互接口。
tut-8/index.mjs

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

首先我们定义一个超时处理程序。
tut-8/index.mjs

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

接下来,我们请求赌注金额 (如果是 Alice ) 或定义 acceptWager 方法 (如果是 Bob ) 。
tut-8/index.mjs

..    // ...
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    };
..    // ...

接下来,我们定义共享的 gethand 方法。
tut-8/index.mjs

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

最后是 seeOutcome 方法。
tut-8/index.mjs

..    // ...
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    })();

最后,我们选择合适的后端功能并等待其完成。


现在我们可以运行

$ make build

然后重建镜像,在该目录的一个终端中

$ make run-alice

在该目录下的另一个终端中:

$ make run-bob

下面是一个运行的例子:

$ 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

另一个实例

$ 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

当然,运行时准确的数字和地址可能不同。


如果要在 Algorand 上测试和运行我们的应用,而不是在以太坊上运行,我们只要编辑 tut-8/docker-compose.yml,然后将第 24 行中的&default-app移动到第 51 行,这样就可以了。


现在,我们的剪刀石头布应用完成了,而且我们不会遭受攻击,超时,平手的影响,同时我们可以在非测试网络上交互运行。
在这一步中,我们为 Reach 程序创建了一个命令行界面。在下一步中,我们将进一步做出一个 Web 界面。

如果你的版本不能正常运行,看看完整的版本: tut-8/index.rshtut-8/index.mjstut-8/package.jsontut-8/Dockerfiletut-8/docker-compose.ymltut-8/Makefile ,并确保您完整地复制了所有内容!

我们可能还需要更改 tut-8/index.rsh 的第 32 行,将 deadline 定义为更大的数字,比如 30 。这是因为 Algorand 不支持 input-enabled 开发网络,在交易出现时只运行轮次,所以超时可能会意外地发生。这种情况在 CPU 负载过高的机器上经常发生。

您知道了吗? :

是非题 : Reach 帮助你为去中心化的应用程序构建自动化测试,但它不支持构建交互式用户界面

答案 : Reach 不会对添加到 Reach 应用程序的前端类型施加任何限制。

上一篇 : 2.7 平局则继续比赛直至判断胜负

下一篇 : 2.9 网页交互