原文链接

    在上面实现的程序中,我们的石头剪刀布会一直运行直到确定最终的赢家为止。在本节中,我们不会改变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.jsontut-8/Dockerfile中看到它们。 我们将自定义其他两个文件。
    首先,让我们看看 tut-8/docker-compose.yml文件:
    tut-8/docker-compose.yml

    1. 1 version: '3.4'
    2. 2 x-app-base: &app-base
    3. 3 image: reachsh/reach-app-tut-7:latest
    4. 4 services:
    5. 5 ethereum-devnet:
    6. 6 image: reachsh/ethereum-devnet:0.1
    7. 7 algorand-devnet:
    8. 8 image: reachsh/algorand-devnet:0.1
    9. 9 depends_on:
    10. 10 - algorand-postgres-db
    11. 11 environment:
    12. 12 - REACH_DEBUG
    13. 13 - POSTGRES_HOST=algorand-postgres-db
    14. 14 - POSTGRES_USER=algogrand
    15. 15 - POSTGRES_PASSWORD=indexer
    16. 16 - POSTGRES_DB=pgdb
    17. 17 ports:
    18. 18 - 9392
    19. 19 algorand-postgres-db:
    20. 20 image: postgres:11
    21. 21 environment:
    22. 22 - POSTGRES_USER=algogrand
    23. 23 - POSTGRES_PASSWORD=indexer
    24. 24 - POSTGRES_DB=pgdb
    25. 25 reach-app-tut-7-ETH-live:
    26. 26 <<: *app-base
    27. 27 environment:
    28. 28 - REACH_DEBUG
    29. 29 - REACH_CONNECTOR_MODE=ETH-live
    30. 30 - ETH_NODE_URI
    31. 31 - ETH_NODE_NETWORK
    32. 32 reach-app-tut-7-ETH-test-dockerized-geth: &default-app
    33. 33 <<: *app-base
    34. 34 depends_on:
    35. 35 - ethereum-devnet
    36. 36 environment:
    37. 37 - REACH_DEBUG
    38. 38 - REACH_CONNECTOR_MODE=ETH-test-dockerized-geth
    39. 39 - ETH_NODE_URI=http://ethereum-devnet:8545
    40. 40 reach-app-tut-7-ETH-test-embedded-ganache:
    41. 41 <<: *app-base
    42. 42 environment:
    43. 43 - REACH_DEBUG
    44. 44 - REACH_CONNECTOR_MODE=ETH-test-embedded-ganache
    45. 45 reach-app-tut-7-FAKE-test-embedded-mock:
    46. 46 <<: *app-base
    47. 47 environment:
    48. 48 - REACH_DEBUG
    49. 49 - REACH_CONNECTOR_MODE=FAKE-test-embedded-mock
    50. 50 reach-app-tut-7-ALGO-test-dockerized-algod-local:
    51. 51 <<: *app-base
    52. 52 environment:
    53. 53 - REACH_DEBUG
    54. 54 - REACH_CONNECTOR_MODE=ALGO-test-dockerized-algod
    55. 55 - ALGO_SERVER=http://host.docker.internal
    56. 56 - ALGO_PORT=4180
    57. 57 - ALGO_INDEXER_SERVER=http://host.docker.internal
    58. 58 - ALGO_INDEXER_PORT=8980
    59. 59 extra_hosts:
    60. 60 - 'host.docker.internal:172.17.0.1'
    61. 61 reach-app-tut-7-ALGO-test-dockerized-algod:
    62. 62 <<: *app-base
    63. 63 depends_on:
    64. 64 - algorand-devnet
    65. 65 environment:
    66. 66 - REACH_DEBUG
    67. 67 - REACH_CONNECTOR_MODE=ALGO-test-dockerized-algod
    68. 68 - ALGO_SERVER=http://algorand-devnet
    69. 69 - ALGO_PORT=4180
    70. 70 - ALGO_INDEXER_SERVER=http://algorand-devnet
    71. 71 - ALGO_INDEXER_PORT=8980
    72. 72 reach-app-tut-7-: *default-app
    73. 73 reach-app-tut-7: *default-app
    74. 74 # After this is new!
    75. 75 player: &player
    76. 76 <<: *default-app
    77. 77 stdin_open: true
    78. 78 alice: *player
    79. 79 bob: *player
    • 第 2 行和第 3 行定义了启动应用程序的服务。如果您在教程中一直停留在同一个目录中,那么第3行将显示tut,而不是 tut-7 。
    • 第 5 行和第 6 行定义了以太坊的 Reach 私有开发者测试网络服务。
    • 第 7 至 24 行定义了为Algorand提供的 Reach private developer 测试网络服务。
    • 第 25 到 73 行定义了允许应用程序在不同网络上运行的服务;包括第 25 行,它定义了 reach-app-tut-7-ETH-live 连接到一个现有网络。
    • 我们还添加了第 73 行到第 77 行来定义一个玩家,它是我们的应用程序,具有开放的标准输入,以及两个名为 alice 和 bob 的实例。

    有了这些,我们就可以运行
    $ docker-compose run WHICH
    其中 WHICH 在 live 实例中是 reach-app-tut-7-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.mjs2.8 交互及自主运行 - 图1

     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';
    ..    // ...
    
    • 第 7 行到第 10 行,问用户是否以 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.rsh, tut-8/index.mjs, tut-8/package.json, tut-8/Dockerfile, tut-8/docker-compose.ymltut-8/Makefile ,并确保您完整地复制了所有内容!

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

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

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