DELEGATECALLのProxy Pattern

概要

今回はDELEGATECALLを使ったProxy Patternについて説明します。コントタクトから別のコントラクトの関数を呼び出す方法として、CALLとDELEGATECALLがあります。この2つの関数の違いについて把握し、その使い所を知ることでDELEGATECALLを理解しやすくなるかと思います。

Proxy Pattern

コントラクトコードは一度ブロックチェーンにデプロイされると変更することができません。そこで変更したい場合は、コントラクトをアップデートする方法としてProxy Patternを使います。

f:id:adrenaline2017:20201113083947p:plain

この方法はストレージとして使用するプロキシコントラクトとロジック定義するコントラクトを分け、プロキシコントラクトからロジックコントラクトの呼び出し先を変えることでアップデートする方法です。この方法を使うとユーザー側から見た際に、プロキシコントラクトの内容が変更しているかの様に動作します。

DELEGATECALLとCALLの違い

下記のコードを使ってCALLとDELEGATECALLの違いを説明したいと思います。このコードではHogeコントラクトからFugaコントラクトのsetNum関数をcallもしくはdelegatecallを使って呼び出しています。

pragma solidity ^0.4.15;

contract Hoge {
    uint public num;
    address public sender;
    
    function callSetNum(address foge, uint _num) public {
        if(!foge.call(bytes4(keccak256("setNum(uint256)")), _num)) revert();
    }
    
    function delegatecallSetNum(address foge, uint _num) public {
        if(!foge.delegatecall(bytes4(keccak256("setNum(uint256)")), _num)) revert();
    }
}

contract Fuga {
    uint public num;
    address public sender;
    
    function setNum(uint _num) public {
        num = _num;
        sender = msg.sender;
    }
}

HogeコントラクトとFugaコントラクトでは変数としてnumとsenderを定義しています。この変数はsetNumで変数に値をセットする為に使用します。

uint public num;
address public sender;

callSetNum関数ではcallでsetNum関数を呼び出し、delegatecallSetNum関数ではdelegatecallでsetNum関数を呼び出しています。

function callSetNum(address foge, uint _num) public {
        if(!foge.call(bytes4(keccak256("setNum(uint256)")), _num)) revert();
    }
    
    function delegatecallSetNum(address foge, uint _num) public {
        if(!foge.delegatecall(bytes4(keccak256("setNum(uint256)")), _num)) revert();
    }

setNum関数を呼び出す際にはfunctionIdを使用します。関数の名前と引数の型を文字列とし、keccak256ハッシュ関数で生成されたハッシュ値の先頭4byteがfunctionIdになります。solidityでは外部の関数を呼び出す際にfunctionIdで呼びですことが出来ます。

bytes4(keccak256("setNum(uint256)") 

このコードを実行した際にcallとdelegatecallでどの様な挙動の違いがあるのかをまとめたものになります。注目して頂きたいのが、delegatecallを実行する際に呼び出しもとのコントラクトの文脈で実行できる(引数も呼び出し元の値を使う)ことです。

呼び出し方 実行方法 msg.sender 引数_n
call Fugaのコントラクトの文脈でsetN関数を実行 Hoge Fugaのストレージ変数nがセット
delegatecall Hogeコントラクトの文脈でsetN関数を実行 Fuga Hogeのストレージ変数nがセット

このdelegatecallの挙動が何を意味するかと言うと、下記の図を参照下さい。ユーザーから見た場合に、先ほど説明したProxy Patternを使いHogeコントラクトにロジックを任せることで、Fugaコントラクトが全て処理をしている様に振舞うことが出来ます。またユーザが直接関わるFugaコントラクトで消費するガスコストを極力抑えると言う意味でも、この方法は有効かと思います。

f:id:adrenaline2017:20201113095801p:plain

ifとrequireの違い

概要

solidityでコードを書く場合に意識せずrequireを使用していました。ふとifとrequireの何が違うのかと思い調べてみました。

結果から言えばifは単に条件分岐をするもので、requireは例外処理としてコントラクトに対する処理を全て取り消してくれます。

実例

requireを使用した実例を挙げます。

このコードでは_inputが100未満の場合、msg.senderに更新されないようにする為にrequireを使いました。 requireが失敗すると、トランザクション全体が元に戻ります。

function some_state_changing_fn (uint256 _input) public returns (bool success)
{
  sender = msg.sender;
  require(_input >= 100);
  input = _input;
  success = true;
}

上記の関数本体にはそれほど関連がないように見えますが、別のコントラクトを呼び出したり、トークンを転送したりする場合には重要です。requireはトランザクション全体が元に戻るので、障害や条件を処理するための非常に安全な方法です。

sendTransactionとcall

概要

solidityの関数を呼び出しにはsendTransactioncallの2パターンがあります。今回はsendTransactioncallの違いと使い分けについて取り上げたいと思います。

sendTransactionとcallの違い

sendTransaction

web3.eth.sendTransaction({
    from: '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe',
    to: '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe',
    value: '1000000000000000'
})
.then(function(receipt){
    ...
});

sendTransactionブロックチェーンネットワークにトランザクションをブロードキャストする関数の呼び出し方です。つまりデータはマイナーによってマイニングされ、そのデータが有効な場合はブロックチェーンに記録され公開されます。

この呼び出しはブロックチェーンの状態を変更するので、Etherを消費します。マイナーがトランザクションをブロックに含めない可能性があるので、非同期で行われます。 そのためトランザクションの即時の戻り値は、常にトランザクションのハッシュになります。 関数へのトランザクションの戻り値を取得するにはeventを使用します。

関数にpureviewが付与されていない場合は、ブロックチェーンの状態を変更する操作なのでsendTransactionで呼び出します。

call

web3.eth.call({
    to: "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", 
    data: "0xc6888fa10000000000000000000000000000000000000000000000000000000000000003"
})
.then(console.log);

callブロックチェーンネットワークにトランザクションをブロードキャストしない関数の呼び出し方です。つまりデータはマイナーによってマイニングされないので、ブロックチェーンには記録されません。

この呼び出しは読み取り専用の操作でなので、Etherを消費しません。 関数は実行され結果が返ってきますが、実行後はすべての状態が破棄されます。callは同期であり、コントラクト関数の戻り値はすぐに返されます。

関数にpureviewが付与されている場合は、呼び出し専用の操作なのでcallで呼び出します。

sendTransactionとcallの使い分け

はじめにcallを使い次にsendTransactionで呼び出すことをお薦めします。

sendTransactionはEtherを使用するのでcallを使って下調べをする必要があります。sendTransactionを使用した場合に例外が発生する可能性が あるので、事前にcallを使ってsendTransactionに 問題があるかデバッグすることが出来ます。

プライベートネットでの動作確認

概要

gethで構築したプライベートネットワークを使い、作成したコントラクトが動作するか確認してみたいと思います。

設定

gethの設定

Homebrewを使いgethをインストールします

$ brew tap ethereum/ethereum
$ brew install ethereum

gethのプライベートネットを起動するにはgenesis.jsonファイルを設定する必要があります。

まずプライベートネットのデータを保管するディレクトリを作成し移動します。

$ mkdir ~/private-net && cd private-net

次にprivate-netディレクトリ内にgenesis.jsonファイルとpasswordファイルを用意します。

$ touch genesis.json
$ touch password

各ファイルの内容は以下の通りです。 genesis.json

{
  "config": {
    "chainId": 15,
    "homesteadBlock": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0
  },
  "coinbase"   : "0x0000000000000000000000000000000000000000",
  "difficulty" : "0x1",
  "extraData"  : "",
  "gasLimit"   : "0x2fefd8",
  "nonce"      : "0x0000000000000042",
  "mixhash"    : "0x0000000000000000000000000000000000000000000000000000000000000000",
  "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
  "timestamp"  : "0x00",
  "alloc": {
    "945cd603a6754cb13c3d61d8fe240990f86f9f8a": { "balance": "500000000000000000000000000" },
    "66b4e7be902300f9a15d900822bbd8803be87391": { "balance": "500000000000000000000000000" },
    "104f0d848da3f760dddadc56fc4ab78305110dba": { "balance": "500000000000000000000000000" },
    "addfaa808c59581f04cdadfc0be28ebfb520e839": { "balance": "500000000000000000000000000" },
    "450a8a99bf5ad49db301f6068c619de2400de6f7": { "balance": "500000000000000000000000000" }
  }
}

password

blah
blah
blah
blah
blah

プライベートネットワークの設定

まずはプライベートネットワークの初期化を行いprivate-netディレクトリにgethディレクトリを作成します。

$ geth --datadir ~/private-net --nodiscover --maxpeers 0 init ~/private-net/genesis.json

次にgethのプライベートネットを起動します。 gethを起動するたびに下記のスクリプトを入力するのは大変なので、geth-start.shファイルを用意し、起動用のシェルスクリプトを作成します。

geth-start.sh

geth --datadir ~/private-net --networkid 15 --nodiscover --maxpeers 0 --mine --minerthreads 1 --rpc --rpcaddr  0.0.0.0 --rpccorsdomain "*" --rpcvhosts "*" --rpcapi "eth,web3,pesonal,net" --ipcpath ~/private-net/geth.ipc --ws --wsaddr  0.0.0.0  --wsapi "eth,web3,personal,net" --wsorigins "*" --unlock 0,1,2,3,4 --password ~/private-net/password --allow-insecure-unlock
$ bash geth-start.sh

プライベートネットを起動するとマイニングが始まります。現在開いてるターミナルにはマイニングの状況がリアルタイムに表示されます。別にもう一つターミナルを立ち上げgeth.ipcファイル(private-netディレクトリにあります)にアタッチしgethコンソールを起動します。

$ geth attach geth.ipc

起動

作成したコントラクト(ここではRoomFactoryというコントラクト用いますが、任意の動作確認したいコントラクトに置き換えて下さい)をtruffleを使いgethで作成したプライベートネットワークにデプロイします。

動作確認する際にABIとコントラクトアドレスが必要になるので、画面で確認出来る様にmigrationファイルを下記の様にします。

deploy_room_factory.js

const RoomFactory = artifacts.require('../contracts/RoomFactory.sol')

module.exports = deployer => {
    deployer.deploy(RoomFactory).then(instance => {
        console.log('ABI:', JSON.stringify(instance.abi))
    })
}

RoomFactoryコントラクトをデプロイします。

$ truffle migrate --rest

すると下記の様な結果が得られます。

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x5fd63c3dc91f64c3ee8963cab14f020d17bb469c079603b564984574e3de4094
  Migrations: 0xf5ad7542173e8944d1ae16b4394eaa34cfda4814
Saving artifacts...
Running migration: 2_deploy_room_factory.js
  Replacing RoomFactory...
  ... 0x38d654c7e25bf331ab43de2de054bf303bf77b7abb9fe6f59e0db704eebf1828
  RoomFactory: 0xcc6cc4d996ec212a1b047b83a4f039e52783bab8
ABI: [{"constant":false,"inputs":[],"name":"unpause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"na
me":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonp
ayable","type":"function"},{"constant":false,"inputs":[],"name":"destroy","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"p
ause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"state
Mutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable",
"type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"}],"name":"destroyAndSend","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"
anonymous":false,"inputs":[{"indexed":true,"name":"_creator","type":"address"},{"indexed":false,"name":"_room","type":"address"},{"indexed":false,"name":"_depositedValue","type":"uint256"}],"n
ame":"RoomCreated","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"},{"anonymous":false,"inputs":[{"
indexed":true,"name":"previousOwner","type":"address"}],"name":"OwnershipRenounced","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previousOwner","type":"address"},{"inde
xed":true,"name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"constant":false,"inputs":[],"name":"createRoom","outputs":[],"payable":true,"stateMutability":"pa
yable","type":"function"}]
Saving artifacts...

RoomFactoryに続く'0xcc6cc4d996ec212a1b047b83a4f039e52783bab8'がコントラクトアドレスになります。またABI:に続く配列のがABIになります。

動作確認

gethのプライベートネットワークにおけるコントラクトの動作確認を行います。動作確認をする際にはマイニングの作業を止める必要がありますので、先程立ち上げたgethコンソールのターミナルを開きマニングをストップさせます。

> miner.stop() 

次にコントラクトオブジェクトを定義します。 先程調べたコントラクトのABIを変数abiに定義します。

> abi = [{"constant":false,"..."type":"function"}]

同様にRoomFactoryのコントラクトアドレスをaddressに定義します。

> address = '0xcc6cc4d996ec212a1b047b83a4f039e52783bab8'

大義したabiとaddressを使いRoomFactoryコントラクトオブジェクトを定義します。

> roomFactory = eth.contract(abi).at(address)

それでは定義したコントラクトオブジェクトを使ってコントラクトの状態呼び出しと変更を行います。

ガスを消費しない状態の呼び出し(今回はpaused関数)にはcallを使用します。

> roomFactory.paused.call()
false

ガスを消費する状態の変更(今回はcreteRoom関数)にはtransactionを使用します。

> roomFactory.createRoom.sendTransaction({from: eth.accounts[0], gas: 100000, value: web3.toWei(0.1, 'ether')})
"0xa1a1239be3c65a4726842bc50b31c628326bb4197d7ff56bcc276e75797108f1"

マイニングを再度開始しトランザクションをブロックに取り込みます。

> miner.start(1)

少し時間を置いてから下記の方法でトランザクションの確認をします。

>eth.getTransaction('0xa1a1239be3c65a4726842bc50b31c628326bb4197d7ff56bcc276e75797108f1')

Dapps開発の基本

概要

Dappsの本格的な開発に入る前に、まず開発の流れを把握する意味でブラウザに「Hello World!」と表示する、簡単なDappsを開発してみたいと思います。

開発環境

  • Solidity
  • Truffle
  • Ganache
  • Metamask

※開発環境の構築についてはこちらの記事を参考にして下さい

開発の流れ

  1. プロジェクトの構築
  2. コントラクトの作成
  3. コントラクトのコンパイル
  4. コントラクトのデプロイ
  5. コントラクトのテスト
  6. フロントエンドの構築

プロジェクトの構築

hello-worldフォルダを作成し、truffle initで初期化します。

$ mkdir hello-world
$ cd hello-world
$ truffle init

hello-worldディレクトリは以下の様な構造になります。

├── contracts
├── migrations
├── test
└── truffle-config.js

コントラクトの作成

コントラクトを作成します。

$ truffle create contract HelloWorld

コントラクトの内容はget()メソッドで「HelloWorld!」を返す簡単なコントラクトです。

pragma solidity ^0.5.0;

contract HelloWorld {
  // string型の変数wordを定義
  string word;

  // 起動時にword変数に「Hello World!」を入れる
  constructor() public {
    word = 'Hello World!';
  }

  // get()関数はword変数を返す
  function get() public view returns (string memory) {
    return word;
  }
}

コントラクトのコンパイル

テストに通ったので、作成したHelloWorld.solをコンパイルします。 コンパイルするとbuildディレクトリ下にHelloWorld.jsonが作成されます。

$ truffle compile

コントラクトのデプロイ

コントラクトをネットワークにデプロイする為に、マイグレーションファイルを作成します。

$ truffle create migration HelloWorld

すると1××××_deploy_contract.jsと言うファイルがmigrationsディレクトリ下に作成されます。 マイグレーションファイルの内容は以下の通りにします。

// artifacts.require()でどのコントラクトとやり取りしたいかをTruffleに伝える
let HelloWorld = artifacts.require("HelloWorld")
// deployer.deploy()でデプロイするコイントラクトを指定
module.exports = function(deployer) {
  deployer.deploy(HelloWorld);
};

作成したマイグレーションファイルを使ってブロックチェーンにデプロイする為にローカルでプライベートチェーンを生成するGanacheを起動します。

f:id:adrenaline2017:20190707084302p:plain

Ganacheの設定に合わせる為にtruffle-config.jsを編集します。

module.exports = {
    networks: {
         development: {
         host: "127.0.0.1",     // ホスト
         port: 7545,            // ポート番号
         network_id: "*", 
        },
},

デプロイは以下の通りです。

$ truffle migrate

コントラクトのテスト

下記のコマンドでtestフォルダ下にhello_world.jsが作成されます。

$ truffle create test HelloWorld

テストの内容は以下の通りです。

// artifacts.require()で指定のコントラクトを呼び出す
const HelloWorld = artifacts.require("HelloWorld");

contract("HelloWorld", function() {
  // 変数を定義
  describe('word method', function(){
    let obj;
    const str = 'Hello World!';
    // テスト毎にHelloWorldコントラクトを作成
    before(async function() {
      obj = await HelloWorld.new();
    });
    // HelloWorldコントラクトのgetメソッドをテスト
    it("should get word", async function () {
      const result = await obj.get();
      assert.equal(result, str);
    });
  });
});

以下のコマンドでテストを走らせます。

$ truffle test test/hello_world.js

テストの実行結果が以下の通りになると成功です。

Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



  Contract: HelloWorld
    word method
      ✓ should get word (68ms)


  1 passing (293ms)

フロントエンドの構築

htmlとjsファイルの作成します。ファイルを作成するディレクトリの構成は以下の通りになります。

├── build
├── contracts
├── hello_world.js
├── index.html
├── migrations
├── test
└── truffle-config.js

index.htmlを以下の通りにします。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>Ðapps - Hello World</title>
    <script type="text/javascript" src="hello_world.js"></script>
  </head>
  <body>
    <div id="contract_result">loading...</div>
  </body>
</html>

hello_world.jsファイルを作成し、フロントのロジックを実装します。hello_world.jsにあるabiはweb3jsとコントラクトの相互関係をとり持つ役割があります。abiとaddressはHelloWorld.jsonにある内容を使うのでコピペして下さい。

var abi = [
  {
    "constant": true,
    "inputs": [],
    "name": "get",
    "outputs": [
      {
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "pure",
    "type": "function",
    "signature": "0x6d4ce63c"
  }
];
var address = "0xd6856EEB9f6B3934E5925414e2c9D911a567e4c3";
   
  window.onload = function() {
    var contract = web3.eth.contract(abi).at(address);
    contract.get((error, result) => {
      document.getElementById("contract_result").textContent = result;
    });
  };

Metamaskを起動します。Metamaskを使うことで、ブラウザでブロックチェーンとやりとりすることができるようになります。Ganacheと接続する為に、Metamaskの設定はGanacheの設定に合わせる必要があります。 f:id:adrenaline2017:20190707230902p:plain

しかし、Metamaskはweb3オブジェクトを生成しないので、このままでは画面に表示することが出来ません。そこで今回は簡易サバーとしてlive-serverを使用します。live-serverはブラウザの自動更新をしてくれる、簡易ローカルサーバーを起動できるツールです。

$ npm i -g live-server
$live-server

ブラウザが起動して画面に「Hello World!」と表示されれば成功です。

これでdappsを開発する流れが一通り掴めたかと思います。 次回は本格的なdappsの開発のチュートリアルに挑戦してみたいと思います。

DApps開発環境の構築と実装

概要

f:id:adrenaline2017:20190706100955j:plain

Solidity, Truffle, Ganache, Metamaskを組み合わせてEhereumで実行出来きるDappsを開発します。

開発環境の構築

以下の項目が開発をする際に必要となる環境になります。

Solidity

Ethereumでスマートコントラクトを開発する為の言語です。構文がJavaScriptに似てるので、非常に親みやすい言語だと思います。 solidityはsolcと呼ばれるSolidityコンパイラによってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換されます。

Truffle

SolidityでDappsを開発する際のフレームワークです。solidityで書いたコードのコンパイル、デプロイ、テストを簡単に行うことが出来ます。

Ganache

プライベートブロックチェーン環境を構築することができるGUIアプリケーションです。本番環境ではgethが必要になりますが、開発環境ではプライベートチェーン使うのでGanacheが向いています。

Visual Studio Code

言わずと知れたコードエディタです。Solidityのプラグインを利用することができます。ターミナルからgit, truffle, Ganacheが利用でき、コントラクトのコンパイル、デプロイ、テストが簡単に出来るのでRemixよりも開発がスムーズに行えます。

Metamask

Ethereum のウォレットで、Chrome拡張機能として提供されています。dappsの秘密鍵管理やトランザクション確認の機能をmetamaskで利用します。

インストール

開発環境の導入方法になります。

Truffle

最新版をインストール

$ npm install -g truffle

バージョンの確認

$ truffle version

Truffle v5.0.13 (core: 5.0.13)
Solidity v0.5.0 (solc-js)
Node v10.14.1
Web3.js v1.0.0-beta.37

ganache

ganacheの公式サイトからファルをダウンロードしてインストールします。

Metamask

Chrome拡張機能なのでMetaMaskから「+CHROMEに追加」をクリックしてプラグインをインストールします。

実装

今回はTruffleの公式サイトに紹介されているチュートリアル「pet shop」を実装していきたいと思います。pet shopではペットの飼い主を決定するためのペットショップのシステムを構築します。

開発の流れ

サーバーサイド

  1. サンプルコードをダウンロード
  2. コントラクトの作成
  3. コンパイル
  4. マイグレションの作成
  5. デプロイ
  6. テストの作成
  7. テスト

サーバーサイド

  1. app.jsを編集
  2. metamaskを起動しプライベートネットに接続

それではまずサーバーサイドの実装を行います。

サーバーサイドの実装

サンプルコードをダウンロード

pet-shopフォルダを作成

$ mkdir pet-shop && cd pet-shop

pet-shopのサンプルをダウンロード

$ truffle unbox pet-shop

コントラクトの作成

「pet-shop」ディレクトリにある「contracts」内に「Adoption.sol」というファイルを作成し、そこにコントラクトを作成します。 $ truffle create contract Adoption コントラクトの内容は以下の通りです。

pragma solidity >= 0.5.0 < 0.7.0;
contract Adoption {
    address[16] public adopters;

    function adopt(uint petId) public returns (uint) {
      require(petId >= 0 && petId <= 15, 'petId must be between 0 and 15');
      adopters[petId] = msg.sender;

      return petId;
    }

    function getAdopters() public view returns (address[16] memory) {

      return adopters;
    }
}

Adoption.solについて説明します。

pragma solidity ^0.5.0;

Solidityはsolcと呼ばれるSolidiコンパイラによってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換されます。そのコンパイルする際に使用するコンパイラのバージョンを指定しています。

contract Adoption {

コントラクトの名前はAdoptionとします。

address[16] public adopters;

address型の配列でアドレスを16個まで保持できるadoptersを定義しています。アクセス修飾子はpublicなのでコントラクト内外部及び継承先で参照することが出来ます。

function adopt(uint petId) public returns (uint) {
    require(petId >= 0 && petId <= 15);
    adopters[petId] = msg.sender;
    
    return petId;
    }

adopt関数を定義しています。この関数はpetIdを引数として取り、requireでpetIdが0〜15であるか確認します。この条件に当てはまる場合、msg.senderをadopters配列に追加します。msg.sederは関数(adopt関数)を呼び出したユーザー若しくはスマートコントラクトのアドレスを参照するものです。今回はpetIdをmsg.seder(自分のアドレス)に紐ずけています。そしてpetIdを返しています。

function getAdopters() public view returns (address[16] memory) {

        return adopters;
    }

getAdopters関数ではAdoption関数で追加したpetIdと、それに紐づいたsddressを返します。返すデータはmemoryで宣言された通り一時的に保存されます。

コンパイル

作成したコントラクトをコンパイラを使ってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換します。

$ truffle compile

マイグレションの作成

変換したバイトコードをEthrereumネットワークにデプロイするために、マイグレーションファイルを作成します。

$ truffle create migration deploy_contracts

「migrations」ディレクトリに「deploy_contracts.js」ファイルを作成します。

var Adoption = artifacts.require("Adoption");
 module.exports = function(deploy) {
 deploy.deploy(Adoption);
}

マイグレーションを行う際にartifacts.require()を介してコントラクトをTruffleに対し指示します。引数はコントラクト名(Adoption)と同じにする必要があります。module.exportsでモジュール化を行いエクスポートします。deploy.deploy(Adoption)では指定した特定のコントラクトをデプロイすることを意味しています。

デプロイ

デプロイする際には開発用ネットワークであるプライベートネットワークをGanacheを使用して実行します。インストールしたGanacheを起動してください。

Truffleを使ってデプロイを行ないます。

$ truffle migrate

テストの作成

「test」ディレクトリに「TestAdoption.sol」ファイルを作成します。

pragma solidity ^0.5.0;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

contract TestAdoption {
    Adoption adoption = Adoption(DeployedAddresses.Adoption());
    uint expectedPetId = 8;
    address expectedAdopter = address(this);
    
    function testUserCanAdoptPet() public {
        uint returnedId = adoption.adopt(expectedPetId);
        Assert.equal(returnedId, expectedPetId, "Adoption of the expected pet should match what is returned.");
    }
    
    function testGetAdopterAddressByPetId() public {
        address adopter = adoption.adopters(expectedPetId);
        Assert.equal(adopter, expectedAdopter, "Owner of the expected pet should be this contract");
    }
    
    function testGetAdopterAddressByPetIdInArray() public {
        address[16] memory adopters = adoption.getAdopters();
        Assert.equal(adopters[expectedPetId], expectedAdopter, "Owner of the expected pet should be this contract");
    }
}

詳細について説明していきます。

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

Assert.solはアサーション関数で、このアサーション関数を使用しテストの合否をチェクします。DeployedAddresses.solはデプロイされたコントラクトのアドレスを取得します。

通常nodeのモジュールは”node modules”にありますが、この2つのファイルは探してもありません。”Assert.sol”はココにあり、”DeployedAddresses.sol”はテスト時に動的に作成されます。

contract TestAdoption {
    Adoption adoption = Adoption(DeployedAddresses.Adoption());

Adoption型のadoption変数にコントラクトのアドレスを格納します。

 uint expectedPetId = 8;
    address expectedAdopter = address(this);

テスト用のpetIdをexpectedPetIdとし8を代入します。またテスト用のアドレスをexpectedAdopterとし、このコントラクトを呼び出したアドレスを代入します。

function testUserCanAdoptPet() public {
        uint returnedId = adoption.adopt(expectedPetId);
        Assert.equal(returnedId, expectedPetId, "Adoption of the expected pet should match what is returned.");
    }

ここではAdoptionコントラクトのadoption関数をテストしています。adoption関数に引数としてexpectedPetId(内容は8)を渡した場合に返ってくる値が同じかどうかチェックしています。つまり指定したpetIdを入れた時に指定したpetIdが返ってくるかテストしています。

function testGetAdopterAddressByPetId() public {
        address adopter = adoption.adopters(expectedPetId);
        Assert.equal(adopter, expectedAdopter, "Owner of the expected pet should be this contract");
    }

ここではAdoptionコントラクトのadopters関数をテストしています。 adopters関数に引数としてexpectedPetId(内容は8)を渡した場合に対応するアドレスを返すかどうかチェックしています。つまり8番目にペットの飼い主のアドレスが登録されているかテストしています。

function testGetAdopterAddressByPetIdInArray() public {
        address[16] memory adopters = adoption.getAdopters();
        Assert.equal(adopters[expectedPetId], expectedAdopter, "Owner of the   expected pet should be this contract");
    }

ここではAdoptionコントラクトのgetAdopters関数をテストしています。 adopters関数に引数としてexpectedPetId(内容は8)を渡した場合に対応するアドレスを返すかどうかチェックしています。つまり配列の8番目にこのコントラクを呼び出したアドレスが入っているかどうかチェックしています。

Truffleのテストはsolidityの他にJavaScriptで書くことも出来ます。

let Adoption = artifacts.require("Adoption");

contract("Adoption", async (accounts) => {
  describe("adopt method", async () => {
    let contract;
    const expectedPetId = 8;
    const expectAdopter = accounts[0];

    beforeEach(async () => {
      contract = await Adoption.new();
    });

    it("adoption of the expected pet should match what is returned", async () => {
      const result = await contract.adopt.call(expectedPetId);
      assert.equal(result, expectedPetId);
    });

    it("owner of the expected pet should be this contract", async () => {
      await contract.adopt(expectedPetId);
      const result = await contract.adopters.call(expectedPetId);
      assert.equal(result, expectAdopter);
    });

    it("owner of the expected pet should be this contract", async () => {
      await contract.adopt(expectedPetId);
      const result = await contract.getAdopters.call();
      assert.equal(result[expectedPetId], expectAdopter);
    });
  });
});

テスト

作成したtestファイルを使ってテストを実行します。

$ truffle test

以上でサーバーサイドの実装は終わりになります。 次にサーバサイドの開発になります。

サーバサイドの実装

JavaScriptAPIであるweb3を使うことで、イーサリアムのノードと通信することができます。フロントの実装では、このAPIを用いてブラウザからコントラクトにアクセスすることが可能になります。

それではapp.js(src/js/app.js)を編集していきます。

App = {
   web3Provider: null,
   contracts: {},
   init: function() {
   $.getJSON('../pets.json', function(data) {
   var petsRow = $('#petsRow');
   var petTemplate = $('#petTemplate');
   for (i = 0; i < data.length; i ++) {
    petTemplate.find('.panel-title').text(data[i].name);
    petTemplate.find('img').attr('src', data[i].picture);
    petTemplate.find('.pet-breed').text(data[i].breed);
    petTemplate.find('.pet-age').text(data[i].age);
    petTemplate.find('.pet-location').text(data[i].location);
    petTemplate.find('.btn-adopt').attr('data-id', data[i].id);
    petsRow.append(petTemplate.html());
   }
  });
   return App.initWeb3();
  },
  initWeb3: function() {
  if (typeof web3 !== 'undefined') {
   App.web3Provider = web3.currentProvider;
  } else {
   App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
  }
   web3 = new Web3(App.web3Provider);
   return App.initContract();
  },
  initContract: function() {
   $.getJSON('Adoption.json', function(data) {
   var AdoptionArtifact = data;
   App.contracts.Adoption = TruffleContract(AdoptionArtifact);
   App.contracts.Adoption.setProvider(App.web3Provider);
   return App.markAdopted();
  });
   return App.bindEvents();
  },
  bindEvents: function() {
   $(document).on('click', '.btn-adopt', App.handleAdopt);
  },
  markAdopted: function(adopters, account) {
   var adoptionInstance;
  App.contracts.Adoption.deployed().then(function(instance) {
   adoptionInstance = instance;
   return adoptionInstance.getAdopters.call();
  }).then(function(adopters) {
   for (i = 0; i < adopters.length; i++) {
    if (adopters[i] !== '0x0000000000000000000000000000000000000000') {
     $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
    }
   }
  }).catch(function(err) {
   console.log(err.message);
  });
   },
  handleAdopt: function(event) {
   event.preventDefault();
   var petId = parseInt($(event.target).data('id'));
   var adoptionInstance;
  web3.eth.getAccounts(function(error, accounts) {
   if (error) {
    console.log(error);
   }
   var account = accounts[0];
   App.contracts.Adoption.deployed().then(function(instance) {
    adoptionInstance = instance;
    return adoptionInstance.adopt(petId, {from: account});
   }).then(function(result) {
    return App.markAdopted();
   }).catch(function(err) {
    console.log(err.message);
   });
  });
   }
  };
  $(function() {
   $(window).load(function() {
    App.init();
   });
  });

詳細について説明していきます。

init: function() {
   $.getJSON('../pets.json', function(data) {
   var petsRow = $('#petsRow');
   var petTemplate = $('#petTemplate');
   for (i = 0; i < data.length; i ++) {
    petTemplate.find('.panel-title').text(data[i].name);
    petTemplate.find('img').attr('src', data[i].picture);
    petTemplate.find('.pet-breed').text(data[i].breed);
    petTemplate.find('.pet-age').text(data[i].age);
    petTemplate.find('.pet-location').text(data[i].location);
    petTemplate.find('.btn-adopt').attr('data-id', data[i].id);
    petsRow.append(petTemplate.html());
   }
  });
   return App.initWeb3();
  },

pets.jsonにある15匹の犬のデータを1匹ずつ呼び出し、index.htmlのpetsRowとpetTemplateに渡し、画面上に表示しています。

initWeb3: function() {
  if (typeof web3 !== 'undefined') {
   App.web3Provider = web3.currentProvider;
  } else {
   App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
  }
   web3 = new Web3(App.web3Provider);
   return App.initContract();
  },

ここではweb3のインスタンス化を行なっています。既存にweb3のインスタンスがあるか確認します。起動している場合はプロバイダを取得しインスタンス化してオブジェクトを作成します。既存にインスタンスがない場合は新規でプロバイダのインスタンスを作成します。

initContract: function() {
   $.getJSON('Adoption.json', function(data) {
   var AdoptionArtifact = data;
   App.contracts.Adoption = TruffleContract(AdoptionArtifact);
   App.contracts.Adoption.setProvider(App.web3Provider);
   return App.markAdopted();
  });
   return App.bindEvents();
  },

ここではコントラクトのインスタンス化を行なっています。Adoptionコントラクトをインスタンス化するために、truffleのtruffle-contractというライブラリのTruffleContract()関数を使用します。この関数の引数にArtifactを入れ、コントラクトをインスタンス化します。そして、web3のインスタンス化で作ったApp.web3Providerをそのコントラクトにセットします。

markAdopted: function(adopters, account) {
   var adoptionInstance;
  App.contracts.Adoption.deployed().then(function(instance) {
   adoptionInstance = instance;
   return adoptionInstance.getAdopters.call();
  }).then(function(adopters) {
   for (i = 0; i < adopters.length; i++) {
    if (adopters[i] !== '0x0000000000000000000000000000000000000000') {
     $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
    }
   }
  }).catch(function(err) {
   console.log(err.message);
  });
   },

ここではボタンが押された際にUIを変更する処理を行なっています。 adoptionInstanceにインスタンスを取得しAdoptionコントラクトのgetAdoption関数を呼び出してブロックチェーンに書かれている情報を返します。1匹ずつ状態を確認し、アドレスが紐づけられていた場合には画面のボタン表示を「Adopte」から「Success」に切り替え、ボタンを押してもトランザクションが発生しない様にします。

handleAdopt: function(event) {
   event.preventDefault();
   var petId = parseInt($(event.target).data('id'));
   var adoptionInstance;
  web3.eth.getAccounts(function(error, accounts) {
   if (error) {
    console.log(error);
   }
   var account = accounts[0];
   App.contracts.Adoption.deployed().then(function(instance) {
    adoptionInstance = instance;
    return adoptionInstance.adopt(petId, {from: account});
   }).then(function(result) {
    return App.markAdopted();
   }).catch(function(err) {
    console.log(err.message);
   });
  });
   }

ここではボタンが押された際にペットを採用する処理を行なっています。押したボタンのpetIdを確認し成功した場合にはadoptionInstanceを呼び出して画面上のUIの変更を行います。失敗した場合はエラーを表示します。 ボタンを押すことでトランザクションを作成しブロックチェーンに情報を書き込みます。

metamaskを起動しプライベートネットに接続

「Ganache」の画面に表示されているINDEX0のPRIVATE KEYをコピーします。インストールしたMetamaskを起動して「アカウントのインポート」から追加を行います。Metamaskのネットワークに新たに「http://127.0.0.1:7545」を追加し選択します。

以上でサーバーサイドとフロントサイドの実装は終わりです。 ターミナルで下記コマンドを実行するとブラウザが立ち上がりdappsを表示することが出来ます。

$ npm run dev

Solidityの構文

Solidityとは

f:id:adrenaline2017:20190706101609p:plain Ethereumでスマートコントラクトを開発する為の言語です。構文がTypeScriptに似てるので、非常に親みやすい言語だと感じました。 solidityはsolcと呼ばれるSolidityコンパイラによってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換されます。

solidityの型には値型と参照型があります。値型は実データそのものを格納し、参照型は実データを参照するためのポインタを格納します。

値型

  • 符号付き整数(int)
  • 符号なし整数(uint)
  • 文字列(string)
  • 論理型(boolean)
  • アドレス(address)
  • バイト型配列(bytes)
  • 関数 (function)
  • 列挙型(enum)

参照型

符号付き整数/符号なし整数

整数には 符号あり整数(int)と符号なし整数(uint)があります。intとuintは変数のbit長を8-256bitで指定出来ます。例えば8bitのuintの場合はuint8と表します。

    // uintに-60を代入するとは出来ない
    uint signedInteger = 60;
    int unsignedInteger = -30;

加算、減算、乗算、徐算、剰余は他のプログラミング言語と同じ様に出来ます。またビット演算子(&, |, ^, ~, <<, >>)比較(<=, <, ==, >, >=)論理演算子(!, &&, ||, ==, !=)計算代入も使用することが可能です。

    uint base = 60;
    uint add = base + 10; // 70
    uint sub = base - 10; // 50
    uint mul = base * 10; // 600
    uint div  = base / 10; //6

文字列

stringには文字列を入れることができます。しかしsolidityのstringの場合、そのまま文字列を比較することができません。Keccak256関数を使って文字列をbytes32型にしてから比較する必要があります。

  
function comparison(string memory _str) public pure returns (bool){
        return keccak256(abi.encode(_str)) == keccak256(abi.encode("hoge"));
    }

アドレス

ウォレットアドレスとコントラクトアドレスがあります。Ethereumのアドレスサイズである20bytesの値を格納します。

function getBalance(address _addr)public view returns (uint){
        return _addr.balance;
}

関数

関数の宣言はfunctionを使います。

//function 関数名(型 変数名){関数の処理}

function companyMember(string _name, uint _age){}

関数を呼び出す場合は以下の通りです。

companyMember("jiro", "37");

関数から戻り値を返す時はアクセス修飾子の後ろにreturns、処理にreturnを書きます。returnsの後には戻り値の型を宣言します。

//function 関数名 (型 変数名)アクセス修飾子 returns(戻り値の型){} 

string word = "Hello World!";
function helloWorld() public view returns (string memory){
    return word;
  } 

他からのアクセスをコントロールする為にsolidutyではpublic, private, internal, externalの4つのアクセス修飾子があります。

・public: コントラクト内外部及び継承されるのも可能

・private: コントラクト内部からのみ参照が可能

・internal: コントラクト内部及び継承されれるのが可能

・external: コントラクト外部からのみ参照が可能

状態修飾子は関数がブロックチェーンとどのように作用し合うのか示すもので、viewとpureの2種類があります。どちらもトランザクションを発行しないので、呼び出しの際にガスは必要ありません。

・view ブロックチェーンに対してデータの保存や変更は一切行わず、読み取りだけを行います。

・pure ブロックチェーンに対してデータの保存や変更を一切行わないだけでなく、読み取りも行いません。

solidityではデータの保存にstorageとmemoryの2種類があります

・storage:ブロックチェーンにデータを永久に保存するのでgathコストがかかる

・memory: コントラクト実行時に一時的にデータを保持するだけなのでgathコストがかからない

privateの場合は関数名の前にアンダーバー(addToArray)が慣習的に使われます。書かなくてもコンパイルエラーにはなりません。memberのアンダーバーは引数が関数内で参照される際に慣習的に使われます)

uint[] members;

function addToArray(uint _member) private {
    members.push(_member);
}

列挙型

列挙型はユーザー定義型を作成する方法です。列挙型には少なくとも1つの定数が必要です。

contract Enum{
    enum Colors{
        Red,
        Blue,
        Green
    }
    Colors color;
    
    function setColor() public {
        color = Colors.Blue;
    }

構造体

複雑なデータ型を作る際に構造体(struct)を用いて表現することが出来ます。 構造体には複数のプロパティを持つデータ型を作ることが可能です。

contract Structure {
    struct Person {
        string name;
        uint age;
    }
    
    Person[] public projects;
    
    function createPerson(string memory _name, uint _age)public{
        Person memory newPerson = Person({
           name: _name,
           age: _age
        });
        projects.push(newPerson);
    }
}

配列

solidityでは2種類の配列があります。

・固定配列 サイズを指定した固定長になります。

uint[2] fixedArray;

・可変配列 サイズが指定されていない可変長になります。

uint[] dynamicArray;

マッピング

マッピングはデータとデータを参照するキーを組み合わせたKVS(キーバリューストア)です。

//mapping (キー => バリュー) アクセス修飾子 マッピング名;

contract Account{
    struct User {
        address addr;
        uint amount;
    }
    mapping(uint => User) public Users;
}

参照

book.ethereum-jp.net