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

概要

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開発環境の構築と実装

概要

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を起動しプライベートネットに接続

それではまずバックエンドの実装を行います。

バックエンドの実装

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

pet-shopフォルダを作成

$ mkdir pet-shop && cd pet-shop

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

$ truffle unbox pet-shop

2. コントラクトの作成

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

pragma solidity ^0.5.0;

contract Adoption {
    address[16] public adopters;
    function adopt(uint petId) public returns (uint) {
    require(petId >= 0 && petId <= 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で宣言された通り一時的に保存されます。

3. コンパイル

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

$ truffle compile

4. マイグレションの作成

変換したバイトコードを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)では指定した特定のコントラクトをデプロイすることを意味しています。

5. デプロイ

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

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

$ truffle migrate

6. テストの作成

「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番目にこのコントラクを呼び出したアドレスが入っているかどうかチェックしています。

7. テスト

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

$ truffle test

以上でバックエンドの実装は終わりになります。 次にフロントエンドの開発になります。

フロントエンドの実装

avaScriptのAPIである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.jsはイーサリアムのノードと通信することのできるJavaScriptAPIで、ブラウザからコントラクトにアクセスすることが可能になります。

既存に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の変更を行います。失敗した場合はエラーを表示します。 ボタンを押すことでトランザクションを作成しブロックチェーンに情報を書き込みます。

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

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

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

$ npm run dev

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ファイルが作成されます。

$ truffle compile

コントラクトのデプロイ

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

$ truffle create migration HelloWorld

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

let HelloWorld = artifacts.require("HelloWorld")
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",     // Localhost (default: none)
         port: 7545,            // Standard Ethereum port (default: none)
         network_id: "*",       // Any network (default: none)
        },
},

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

$ truffle migrate

フロントエンドの構築

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は以下の通りです。

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の開発のチュートリアルに挑戦してみたいと思います。

solidityの構文

Solidityとは

f:id:adrenaline2017:20190706101609p:plain Ethereumでスマートコントラクトを開発する為の言語です。構文がJavaScriptに似てるので、非常に親みやすい言語だと思います。 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;
}

Remixをgethの接続

Remixとは

f:id:adrenaline2017:20190706101334p:plain

Solidityでコントラクトを開発する為のIDEです。 Remixはローカルで立ち上げる方法とクラウドIDEの2種類があります。

gethとは

「Go Ethereum」の略で、Ethereumのフルノードを操作するためのクライアントソフトです。BitcoinでいうBitcoin coreの様なものです。

Gethの設定

$ brew tap ethereum/ethereum
$ brew install ethereum

Gethの初期化

eth_private_netフォルダにgenesis.jsonファイルを作成

$ mkdir -p /Users/ユーザー名/Documents/eth_private_net
$ cd /Users/ユーザー名/Documents/eth_private_net
$ vim genesis.json
{
"nonce": "0x0000000000000042",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"difficulty": "0x400",
"alloc": {},
"coinbase": "0x3333333333333333333333333333333333333333",
"timestamp": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x",
"gasLimit": "0x8000000",
"config": {}
}

Gethの初期化

geth --datadir /Users/ユーザー名/Documents/eth_private_net init /Users/ユーザー名/Documents/eth_private_net/genesis.json

Gethの起動

geth --networkid "10" --nodiscover --datadir "/Users/ユーザー名/Documents/eth_private_net" console 2>> /Users/ユーザー名/Documents/eth_private_net/geth_err.log

マイニング

アカウント作成

> personal.newAccount()

アカウントの確認

> eth.accounts

コインベースアカウントの確認

> eth.coinbase

マイニングの開始

> miner.start()

マイニングの確認

> eth.mining

アカウント1番目の残高

> web3.fromWei(eth.getBalance(eth.accounts[0]),"ether")

マイニングの停止

> miner.stop()

送金

アカウントのアンロック

personal.unlockAccount(eth.accounts[0])

アカウント1からアカウント2に5ether送金

> eth.sendTransaction({from:eth.accounts[0], to:eth.accounts[1], value:web3.toWei(5, "ether")})

Remixとgethの接続

Gethの起動

geth --networkid "10" --nodiscover --datadir "/Users/ユーザー名/Documents/eth_private_net" --mine --unlock 0 --rpc --rpcaddr "localhost" --rpcport "8545" --rpccorsdomain "*" console 2>>/Users/ユーザー名/Documents/eth_private_net/geth_err.log

マイニングの開始

> miner.start()

Remixのアクセス

Remixブラウザにアクセス

Environment項目の「Web3 Provider」を選択

Web3 Provider Endpointに「http://localhost:8545」 が設定されているのを確認し「OK」をクリック

新規にコントラクトを作成

pragma solidity ^0.4.11;
 
contract HelloAddress {
    
    address public owner;
    
    function HelloAddress(){
        owner = msg.sender;
    }
   
    function getOwnerAddress() constant returns (address){
        return owner;
    }
    
}

デプロイをクリック

「getOwnerAddress」ボタンをクリックしアドレスが表示されるか確認