LoginSignup
6
2

More than 1 year has passed since last update.

簡単なDAppを作ってみる Hello, world編

Last updated at Posted at 2022-03-03

前回はDAppを作成するための開発環境構築を行いました。
今回は構想したDAppを作成する前にHello, world的なDAppを作りたいと思います。

目次

(全て書き終わったら作成)

truffleプロジェクト作成

まず、truffleプロジェクトのディレクトリを作成します。ここでは「helloworld_solidity」という名前で作成しました。

$ mkdir helloworld_solidity
$ cd helloworld_solidity

次にtruffleプロジェクトを初期化します。

$ truffle init

Starting init...
================

> Copying project files to <パス>/helloworld_solidity

Init successful, sweet!

Try our scaffold commands to get started:
  $ truffle create contract YourContractName # scaffold a contract
  $ truffle create test YourTestName         # scaffold a test

http://trufflesuite.com/docs

ログを見ると、
truffle create contract YourContractName でコントラクトのひな型を作成でき、
truffle create test YourTestName でテストのひな型を作成できるみたいです。

truffle initすることで、以下のようなフォルダ/ファイルが作成されました。

|
│  truffle-config.js          # truffleに関する設定ファイル。接続するブロックチェーンネットワーク情報など。
│
├─contracts                   # コントラクトを入れるフォルダ
│      Migrations.sol         # マイグレーションを管理するコントラクト
│
├─migrations                  # マイグレーションを入れるフォルダ
│      1_initial_migration.js # Migrationsコントラクトをデプロイするためのスクリプト
│
└─test
        .gitkeep

テストの追加

まずはテストを追加します。
テストスクリプトにはJavascriptで書く方法とSolidityで書く方法があるようです。Javascriptで書かれることが多いようなので、私もそれに倣います。

$ truffle create test HelloWorld

するとtestフォルダにhello_world.jsというひな型ファイルが生成されます。
内容は以下のようになっています。

hello_world.js
const HelloWorld = artifacts.require("HelloWorld");

/*
 * uncomment accounts to access the test accounts made available by the
 * Ethereum client
 * See docs: https://www.trufflesuite.com/docs/truffle/testing/writing-tests-in-javascript
 */
contract("HelloWorld", function (/* accounts */) {
  it("should assert true", async function () {
    await HelloWorld.deployed();
    return assert.isTrue(true);
  });
});

この時点で一度テストを実行してみます。

$ truffle test test/hello_world.js


Compiling your contracts...
===========================
> Compiling .\contracts\Migrations.sol
> Artifacts written to C:\Users\...\AppData\Local\Temp\test--24292-MtYK9AKz3x5k
> Compiled successfully using:
   - solc: 0.8.11+commit.d7f03943.Emscripten.clang

Error: Could not find artifacts for HelloWorld from any sources
    ...
Truffle v5.4.31 (core: 5.4.31)
Node v16.13.2

Error: Could not find artifacts for HelloWorld from any sourcesからHelloWorldというコントラクトが見つけられなかったことがわかります。
では、HelloWorldコントラクトを作成します。

$ truffle create contract HelloWorld

実行すると、contractsフォルダにHelloWorld.solが生成されました。
内容は以下のようになっています。

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract HelloWorld {
  constructor() public {
  }
}

再びテストを実行してみます。

truffle test

Compiling your contracts...
===========================
> Compiling .\contracts\HelloWorld.sol
> Compiling .\contracts\Migrations.sol
> Compilation warnings encountered:

    Warning: Visibility for constructor is ignored. If you want the contract to be non-deployable, making it "abstract" is sufficient.
 --> project:/contracts/HelloWorld.sol:5:3:
  |
5 |   constructor() public {
  |   ^ (Relevant source part starts here and spans across multiple lines).


> Artifacts written to C:\Users\...\AppData\Local\Temp\test--17712-7MDSFxi55Q7V
> Compiled successfully using:
   - solc: 0.8.11+commit.d7f03943.Emscripten.clang



  Contract: HelloWorld
    1) should assert true
    > No events were emitted


  0 passing (114ms)
  1 failing

  1) Contract: HelloWorld
       should assert true:
     Error: HelloWorld has not been deployed to detected network (network/artifact mismatch)
...

次はHelloWorldコントラクトをデプロイしていないと言われています。
マイグレーションスクリプトを作成します。truffle create migrationコマンドが使えました。

$ truffle create migration HelloWorld

実行すると、migrationsフォルダに私の環境では1645156827_hello_world.jsが生成されました。
内容は以下のようになっています。

1645156827_hello_world.js
module.exports = function(_deployer) {
  // Use deployer to state migration tasks.
};

これを以下のように書き換えます。

1645156827_hello_world.js
const HelloWorld = artifacts.require("HelloWorld");

module.exports = function (_deployer) {
  // Use deployer to state migration tasks.
  _deployer.deploy(HelloWorld);
};

もう一度テストを実行します。

$ truffle test

Compiling your contracts...
===========================
> Compiling .\contracts\HelloWorld.sol
> Compiling .\contracts\Migrations.sol
> Compilation warnings encountered:

    Warning: Visibility for constructor is ignored. If you want the contract to be non-deployable, making it "abstract" is sufficient.
 --> project:/contracts/HelloWorld.sol:5:3:
  |
5 |   constructor() public {
  |   ^ (Relevant source part starts here and spans across multiple lines).


> Artifacts written to C:\Users\...\AppData\Local\Temp\test--24388-WAfRWgIAgWvW
> Compiled successfully using:
   - solc: 0.8.11+commit.d7f03943.Emscripten.clang



  Contract: HelloWorld
     should assert true


  1 passing (135ms)

今度は成功しました。

HelloWorldの作成

「Hello, world」と返すコントラクトを作ります。
まずは、テストから作成します。

hello_world.js
...
contract("HelloWorld", function (/* accounts */) {
  ...

  describe("helloworld()", () => {
    it("return Hello, world", async () => {
      const helloworld = await HelloWorld.deployed();
      const expected = "Hello, world"; // 期待する値
      const ret = await helloworld.helloworld();
      assert.equal(ret, expected, "Hello, world was returned!");
    })
  });

});

テストを実行すると、以下のエラー内容で返ってきます。

$ truffle test

Compiling your contracts...
===========================
> Compiling .\contracts\HelloWorld.sol
> Compiling .\contracts\Migrations.sol
> Compilation warnings encountered:

...



  Contract: HelloWorld
     should assert true (46ms)
    helloworld()
      1) return Hello, world
    > No events were emitted


  1 passing (315ms)
  1 failing

  1) Contract: HelloWorld
       helloworld()
         return Hello, world:
     TypeError: helloworld.helloworld is not a function
      at Context.<anonymous> (test\hello_world.js:18:36)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)

helloworld.helloworldが関数ではないこと示しています。つまりそのような関数がないことを言っています。
HelloWorldコントラクトにhelloworld関数を作成します。

HelloWorld.sol
...
contract HelloWorld {
    ...

    function helloworld() external pure returns (string memory) {
        return "Hello, world";
    }
}

helloworld関数には修飾子がついています。以下に簡単にまとめます。

修飾子 外部からアクセス 内部からアクセス 派生したコントラクトからアクセス
external × ×
public
internal ×
private × ×
修飾子 データ書込み データ読込み
pure × ×
view ×
payable × ×
なし
修飾子 ブロックチェーンに確保 実行時のみ確保
storage ×
memory ×
calldata ×
なし(storage) ×

※ calldataは外部(external)から呼び出され、データ型がmapping, struct, string, arrayなどの参照型の時につける。

テストを実行すると以下のように表示され成功します。

truffle test

Compiling your contracts...
===========================
> Compiling .\contracts\HelloWorld.sol
> Compiling .\contracts\Migrations.sol
> Compilation warnings encountered:
...
  Contract: HelloWorld
     should assert true
    helloworld()
       return Hello, world


  2 passing (236ms)

値の設定と状態変数

次に先ほどのHelloWorldコントラクトを改造して、任意の値を設定できるようにします。
まずはテストから作成します。

hello_world.js
...
contract("HelloWorld", function (/* accounts */) {
  ...

});

contract("Set text helloworld", function (/* accounts */) {
  describe("setHelloworld(string)", () => {
    it("set text and return Hello, DApp", async () => {
      const helloworld = await HelloWorld.deployed();
      const expected = "Hello, DApp"; // 期待する値
      await helloworld.setHelloworld(expected); // 値を設定
      const ret = await helloworld.helloworld();
      assert.equal(ret, expected, "Hello, DApp was returned!");
    });
  });

});

contract関数をもう一つ設けることで、他のテストに影響を受けないようにしています。
テストを実行すると、やはりsetHelloworldという関数は存在しないと言われます。
setHelloworld関数の定義のみを以下のように作成します。

HelloWorld.sol
...
contract HelloWorld {
    string private _text = "Hello, world"; // 状態変数(storageに保存)

    constructor() public {}

    function helloworld() external view returns (string memory) {
        return _text; // 状態変数の値を返す
    }

    function setHelloworld(string calldata text) external {
        _text = text; // 値を設定
    }
}

Ownableにする

Ownableを導入し、コントラクトを所有している所有者のみテキストを変更できるようにします。
まずは、所有者の存在テストを作成します。

hello_world.js
...
contract("HelloWorld", function (/* accounts */) {
  ...
  describe("owner()", () => {
    it("return the addres of owner", async () => {
      const helloworld = await HelloWorld.deployed();
      const owner = await helloworld.owner(); // コントラクトの所有者を返す
      assert(owner, "the current owner");
    });
  });

});
...

テストを実行しますが、例によってowner関数が存在しません。
HelloWorldコントラクトにコントラクトの所有者を返すowner関数を作成します。

HelloWorld.sol
...
contract HelloWorld {
    ...
    address private _owner;
    ...
    function owner() public view returns (address) {
        return _owner;
    }
}

状態変数_ownerにはaddress修飾子がついています。このほかにaddress payableという修飾子もあります。違いはaddress payable修飾子は、transferやsendを使用してETHを移動させ、また受け取ることができますが、address修飾子にはできません。
テストを実行すると成功します。
次に所有者のアドレスがコントラクトのデプロイに使われたアカウントかテストするスクリプトを作成します。

hello_world.js
contract("HelloWorld", function (accounts) { // accountsを引数に指定!
  ...
  describe("owner()", () => {
    it("return the addres of owner", async () => {
      const helloworld = await HelloWorld.deployed();
      const owner = await helloworld.owner(); // コントラクトの所有者を返す
      assert(owner, "the current owner");
    });

    it("is deployed account?", async () => {
      const helloworld = await HelloWorld.deployed();
      const owner = await helloworld.owner(); // コントラクトの所有者を返す
      const expected = accounts[0]; // デプロイしたアカウントを取得
      assert.equal(owner, expected, "owner account == deployed account");
    });
  });

});

accounts変数は、使用できるアカウントの配列です。
テストを実行すると、ownerには何も設定していないので、ownerとexpectedは一致せず失敗します。
コントラクトのコンストラクト(ややこしい!)にownerを設定する処理を入れます。

HelloWorld.sol
contract HelloWorld {
    ...
    constructor() {
        _owner = msg.sender;
    }
    ...
}

これでテストは成功します。
次に、所有者以外の他のアカウントから実行した場合のテストも作成します。

hello_world.js
...
contract("Set text helloworld", function (accounts) {
  ...
  describe("set text by another account", () => {
    it("Cannot set text", async () => {
      const helloworld = await HelloWorld.deployed();
      const expected = helloworld.helloworld(); // 期待する値
      try {
        await helloworld.setHelloworld("Another account", { from: accounts[1] }); // 他のアカウントでテキストを設定
      } catch (err) {
        const errorMessage = "Ownable: caller is not the owner";
        assert.equal(err.reason, errorMessage, "Cannot set text");
        return;
      }
      assert(false, "Cannot set text");
    });
  });

});

コントラクトにmodifierを追加します。

HelloWorld.sol
...
contract HelloWorld {
    ...
    function setHelloworld(string calldata text) external onlyOwner { // 制約を設定
        _text = text; // 値を設定
    }

    ...

    modifier onlyOwner() {
        require(msg.sender == _owner, "Ownable: caller is not the owner");
        _;
    }
}

これでエラー内容が一致するのでテストは成功します。
なお、OwnebleはOpenZeppelinを使用すれば以下のように簡単に導入できます。
まずはOpenZeppelin/contractsをインストールします。

npm install @openzeppelin/contracts

次にコントラクトを以下のようにします。

HelloWorld.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "@openzeppelin/contracts/access/Ownable.sol";

contract HelloWorld is Ownable {
    string private _text = "Hello, world"; // 状態変数(storageに保存)

    // address private _owner;

    // constructor() {
    //     _owner = msg.sender;
    // }

    function helloworld() external view returns (string memory) {
        return _text; // 状態変数の値を返す
    }

    function setHelloworld(string calldata text) external onlyOwner {
        _text = text; // 値を設定
    }

    // function owner() public view returns (address) {
    //     return _owner;
    // }

    // modifier onlyOwner() {
    //     require(msg.sender == _owner, "Ownable: caller is not the owner");
    //     _;
    // }
}

UI作成

HelloWorldコントラクトをWebUIから呼び出してみます。
まずはプロジェクトを作成します。

$ mkdir web
$ cd web
$ truffle unbox react

Starting unbox...
=================

 Preparing to download box
 Downloading
...
 Cleaning up temporary files
 Setting up box

Unbox successful, sweet!

Commands:

  Compile:              truffle compile
  Migrate:              truffle migrate
  Test contracts:       truffle test
  Test dapp:            cd client && npm test
  Run dev server:       cd client && npm run start
  Build for production: cd client && npm run build

次にGanacheを立ち上げます。
image.png
「NEW WORKSPACE」を選択し、名前とプロジェクトのtruffle-config.jsを選択します。そして、「SAVE WORKSPACE」を選択します。
image.png
アカウント一覧が表示されます。右上の歯車アイコンを選択し、「PORT NUMBER」を8545に変更します。
image.png
そして、ニーモニックをコピーして、MetaMaskにアカウントをインポートします。インポートした後、設定の詳細からテストネットワークの表示を有効化すると、「LOCALHOST 8545」が見えるようになるので、選択します。
また、truffle-config.jsを以下のように設定します。

truffle-config.js
const path = require("path");

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
  contracts_build_directory: path.join(__dirname, "client/src/contracts"),
  networks: {
    develop: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*"
    }
  }
};

一度この状態でコンパイルし、実行してみます。

$ truffle compile
$ truffle migrate --network develop
$ cd client
$ npm install
$ npm run start

以下のようにデフォルトのSimpleStorageコントラクトが実行されます。
image.png

まずは、これらのデフォルトコントラクトを削除します。また、contracts/Migrations.solも若干古そうなので削除します。ついでにopenzeppelinもインストールしておきます。

$ cd ../
$ rm contracts/SimpleStorage.sol
$ rm contracts/Migrations.sol
$ rm migrations/2_deploy_contracts.js
$ npm install @openzeppelin/contracts

代わりに先ほど作ったHelloWorld.solとMigrations.solをcontractsフォルダへ、HelloWorldのマイグレーションファイルをmigrationsフォルダへコピーします。
solidityのバージョンが0.5.16となっているので、truffle-config.jsのcompilersを以下のように変更します。

truffle-config.js
const path = require("path");

module.exports = {
  networks: {
    development: {
      host: "localhost",
      port: 8545,
      network_id: "*", // Match any network id
      gas: 5000000
    }
  },
  compilers: {
    solc: {
      version: "0.8.11",
      settings: {
        optimizer: {
          enabled: true, // Default: false
          runs: 200      // Default: 200
        },
      }
    }
  },
  contracts_build_directory: path.join(__dirname, "client/src/contracts")
};

注意
以下の記述を挿入した理由は、後述するApp.jsでコントラクトをインポートする際srcフォルダから見てビルドされたコントラクトを参照できないエラーを解消するためです。

const path = require("path");
...
contracts_build_directory: path.join(__dirname, "client/src/contracts")

これで一度、truffle compileとtruffle migrationが成功するか確認しておきます。

$ truffle compile
$ truffle migration --network develop

次にApp.jsを以下のように修正します。

App.js
import React, { Component } from "react";
import HelloWorld from "./contracts/HelloWorld.json";
import getWeb3 from "./getWeb3";

import "./App.css";

class App extends Component {
  state = { text: '', web3: null, accounts: null, contract: null };

  componentDidMount = async () => {
    try {
      // Get network provider and web3 instance.
      const web3 = await getWeb3();

      // Use web3 to get the user's accounts.
      const accounts = await web3.eth.getAccounts();

      // Get the contract instance.
      const networkId = await web3.eth.net.getId();
      const deployedNetwork = HelloWorld.networks[networkId];
      const instance = new web3.eth.Contract(
        HelloWorld.abi,
        deployedNetwork && deployedNetwork.address,
      );

      // Set web3, accounts, and contract to the state, and then proceed with an
      // example of interacting with the contract's methods.
      this.setState({ web3, accounts, contract: instance }, this.runExample);
    } catch (error) {
      // Catch any errors for any of the above operations.
      alert(
        `Failed to load web3, accounts, or contract. Check console for details.`,
      );
      console.error(error);
    }
  };

  runExample = async () => {
    const { accounts, contract } = this.state;

    const ret = await contract.methods.helloworld().call(); // HelloWorldコントラクトのhelloworld関数を呼び出す

    this.setState({ text: ret });
  };

  render() {
    if (!this.state.web3) {
      return <div>Loading Web3, accounts, and contract...</div>;
    }
    return (
      <div className="App">
        <h1>HelloWorld Contract!?</h1>
        {this.state.text}
      </div>
    );
  }
}

export default App;

HelloWorldコントラクトをインポートし、helloworld関数を呼び出しています。結果をsetStateに格納し、表示部分で取り出しています。

情報
アカウントアドレスを設定してくださいのようなエラーが最初表示されていたため、

  runExample = async () => {
    const { accounts, contract } = this.state;
    contract.options.address = "0x4a1Aa62569d9F0703cC095FFb6ed72b2fCb7a6Bd"; // <- ここ

    const ret = await contract.methods.helloworld().call(); // HelloWorldコントラクトのhelloworld関数を呼び出す

    this.setState({ text: ret });
  };

としていましたが、再びエラーが出たため、削除すると正常に表示されるようになりました。何だったのでしょうか。

(追記)
@HOGATA0530 様のコメントよりtruffle migration --network develop --reset を行うことでエラーが出なくなったとのことです。もし、エラーに遭遇した方はお試しください。情報ありがとうございます。
以下引用

ここに関して正確なところでは無いのですが、自分も最初はエラーになったため確認していたら、deployedNetworkがundefinedになっていたため、resetつけてmigrationをやり直したらエラー出なくなりました。
truffle migration --network develop --reset

実行すると、以下のような画面が表示されます。
image.png

フォームを作ってテキストを設定できるようにします。

App.js
...
class App extends Component {
  ...
  formSubmitHandler = async () => {
    const text = document.getElementById("text").value;
    const { accounts, contract } = this.state;
    const updatedText = await contract.methods.setHelloworld(text).send({ from: accounts[0] });
    this.setState({ text: text });
    console.log(updatedText);
  }

  render() {
    if (!this.state.web3) {
      return <div>Loading Web3, accounts, and contract...</div>;
    }
    return (
      <div className="App">
        <h1>HelloWorld Contract!?</h1>
        {this.state.text}
        <form>
          <label>
            Set text:
            <input type="text" id="text" />
          </label>
        </form>
        <button onClick={this.formSubmitHandler}>Submit</button>
      </div>
    );
  }
}

フォームにテキストを設定し「Submit」を押すと、ブロックチェーンに保存され更新しても保存されていることがわかります。
image.png

感想

たかがHello, worldされどHello, worldという感じで、ボリューミーで慣れていないためなかなか躓くところもあり勉強になりました。

まとめ

truffle, ganache, reactを使用した簡単なHello, worldコントラクトプロジェクトを作ることで、Dapp作成の基本操作を学びました。
truffleを使用することで、簡単にプロジェクト作成、開発、テストを行うことができ、react boxなどのテンプレートも充実していることがわかりました。

参考

6
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2