Help us understand the problem. What is going on with this article?

スマートコントラクト開発の基本

More than 1 year has passed since last update.

2013年にウォータールー大学の学生であったVitalik Buterin により提唱された「Ethereum」は、スマートコントラクトとブロックチェーンの関係性をさらに発展させました。
今回はEthereumとEthereumのスマートコントラクトを開発する言語であるSolidityを学んでいこうと思います。

Ethereum

Ethereumはスマートコントラクト構築するためのプラットフォームのことです。
また、Ethereumでプログラムを実行するためにはコストがかかりそのコストはガスと呼ばれます。
チェーンの採用は「生成に一番コストがかかっているブロック」をチェーンとして採用する方式になっています。

これらの特徴はBitcoinを大きく異なる点です。
Bitcoinは、お金の送金を早くするために作られたもので、チェーンは一番長いチェーンが採用されます。

Solidity

Ethereum上のスマートコントラクトを開発するための言語です。言語的な特徴はJavaScriptに似ていると言われ、高級言語になります。
ですが、最近Version5がリリースされ、仕様がかなり変わったと言われています。

Solidityの他にもVyperと呼ばれるスマートコントラクトを開発する言語もあり、こちらはPythonの特徴に似ていると言われています。

ゾンビゲーム

CryptZombies」と呼ばれる、ゾンビバトルゲームでSolidityの基礎を学ぼうと思います。

以下、ゾンビゲームをやって復習程度に見る感じにまとめています。

・Lesson 1
ゾンビを作るためのSolidityの基本的なコード書いていきます。1時間くらいで終わるので手軽に取り組めます。

・Lesson 2
Lesson 1 で単発で学んだ要素を他の要素と組み合わせたりする形でコードを書いていきます。隔週内容としては多くないのですが学んだことをしっかりと理解してないと難しい内容でした。理解に時間がかかり2時間程度かかりました。

・Lesson3
ゲームに追加機能をつける形で実際にどう実装していくかを学べました。また簡単なガスコストの節約に関しても学べます。1時間半程度で終わります。

・Lesson4
Lesson3と同様に追加機能をつけていく形で学びました。条件分岐やカウントがメインでした。1時間半程度で終わります。

・Lesson5
ERC721の規格とトークンの交換、Libraryの使い方に関して学んだ。1時間程度で終わります。

・Lesson6
スマートコントラクトのフロントの作成をしていきます。

Lesson1

コントラクト

基本ブロック

contract HelloWorld {
}

Version Pragme

ソースコードは全てpragma solidity ^ ;からはじまり、バージョンを指定する

状態変数

コントラクト内に永遠に保管されるデータベースのようなもの

contract Example {
  // この部分がブロックチェーン上に記載される
  uint example = 100;
}

演算

演算は基本的にJSなどと変わらない
加算(足し算): x + y
減算(引き算): x - y,
乗算(掛け算): x * y
除算(割り算): x / y
剰余(余り): x % y
累乗:x ** y

構造体

複雑なデータ型を作成できるように構造体が用意されている。

struct Zombie {
 string name;
  uint dna;
}

配列

固定長配列:配列の長さが決まっている

Zombie [2] zombies;

可変長配列:配列の長さが決まっていない

Zombie [] zombies;

Public配列
配列でpublicを宣言すると、getterメソッドが作成される。
また他のコントラクトも配列を読める(*書き込むことはできない)

関数の宣言

stringとuintの2つのパラメータをもつ関数

function createZombie(string _name, uint _dna) {
}

*関数はデフォルトでpublicとなっているためprivateにしたい場合は追記する
その際に関数を_(アンダースコア)から始めるようにする
*空白を開けるところは必ず開ける

関数の呼び出し

createZombie("Zombie1", 12345);

構造体の作成と配列の操作

// 新しい構造体の生成:
Zombie zombie1 = Zombie("Zombie1",12345);
// それを配列に格納する:
zombies.push(zombie1);

上記のコードを一行で表す。

zombies.push(zombie("Zombie1",12345));

イベント

何かが起った時にアプリのフロントエンドに届けるもの

// イベントの宣言
event NewZombie(uint id, string name, uint dna);

function CreateZombie(string name, uint dna) public {
  uint id = dna -15;
  // 関数が呼ばれたことをアプリに伝えるためにイベントを発生させる:
  NewZombie(id, name, dna);
}

Lesson2

アドレス address

銀行号座番号のような、ある特定のユーザーが所有しているもの

マッピング mapping

何かと何かを関連付けするもの
要復習

mapping (uint => address) public zombieToOwner;

uintがキーとなり addressがバリューとなる

msg.sender

グローバル変数の一つで、関数を呼び出したユーザーのaddressを参照できる。

*id下にmsg.senderを格納してzombieToOwnerマッピングを更新する

zombieToOwner[id] = msg.sender;

require

条件を満たさないものをエラーとして処理する

継承

コントラクトは継承することができる

contract 新しいコントラクト is 継承されるコントラクト {    
}

storageとmemory

storage:ブロックチェーン上に永久に保存される変数
memory:一時的に保存される変数

internalとexternal

・internal:基本はprivateと同じだが、継承先のコントラクトからでもアクセスできるようになる
・external:基本はpublicと同じだがコントラクトの外からだけ呼び出すことができる。

Interface

他人のコントラクトから関数を宣言することができる。
使うときは関数宣言の最後に;(セミコロン)を使う。(括弧などは利用しない)

contract KittyInterface  {
    function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
    );       
}

複数の返り値の処理

function multipleReturns() internal returns(uint a, uint b, uint c) {
  return (1, 2, 3);
}

function processMultipleReturns() external {
  uint a;
  uint b;
  uint c;
  // 複数に割り当てる方法:
  (a, b, c) = multipleReturns();
}

// そのうちの一つの値だけ欲しい時:
function getLastReturnValue() external {
  uint c;
  (,,c) = multipleReturns();
}

Lesson 3

Immutability of Contracts

イーサリアム上に一度デプロイすると後からコントラクトの変更ができなくなる。

ownable

権限を持つユーザーのみ利用できるようにするもの

ownableの基本的な使い方
1.コントラクトが作られた時、コンストラクタがowner を msg.sender (実行した人物だ)に設定する。
2.onlyOwner修飾子を追加して、ownerだけが特定の関数にアクセスできるように設定する。
3.新しいownerにコントラクトを譲渡することも可能

・コンストラクタ(function Ownable()):コントラクトの作成時に1度だけ実行される

関数修飾子

他の関数の編集に利用される

modifier aboveLevel(uint _level, uint _zombieId) {
  require(zombies[_zombieId].level >= _level);
  _;
}

function changeName(uint _zombieId, uint _newName) aboveLevel(2, _level) {
  require(msg.sender == zombieToOwner[_zombieId]);
  zombies[_zombieId].name == _newName;
}

modifierで定義したabovelevelchangeNameの関係修飾子として使っている。
また、aboveLevel(2, _zombieId)2_zombieIdをパラメーターとして渡している。
関数の使い方と基本変わらない。

View関数

View関数はブロックチェーン上での変更と関係ないのでガスが不要

memory内での配列の宣言

storageはガスコストが高いのでなるべくmonoryを使う様にする

uint[] memory values = new uint[](3);

memoryで配列を使う場合は必ず長さを指定する。

for

基本はJavaScriptと変わらない

uint counter = 0;
  for (uint i = 0; i < zombies.length; i++) {
    if (zombieToOwner[i] == _owner) {
      // 配列に格納する
      result[counter] = i;
    }  
  }

Time Units

seconds、 minutes、 hours、 days、weeks 、yearの単位が用意されていて、
それぞれuintの秒数に変化される。

structを引数として渡す

structのstorageポインタにより、関数の引数として渡すことができる

function sample(Zombie storage _zombie) internal {
} 

Lesson4

payable修飾詞

関数を実行するためにEtherの支払いをさせることも可能
msg.value = コントラクトにどれだけのお金が貯まったか

function buySomething() external payable {
  require(msg.value == 0.001 ether);
}

withdraw関数

コントラクトに貯まったEtherを取り出す。

function withdraw() external onlyOwner {
}

Lesson5

ERC721

ERC721を実装する際には以下のものを定義していく必要がある

contract ERC721 {
  event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
  event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

  function balanceOf(address _owner) public view returns (uint256 _balance);
  function ownerOf(uint256 _tokenId) public view returns (address _owner);
  function transfer(address _to, uint256 _tokenId) public;
  function approve(address _to, uint256 _tokenId) public;
  function takeOwnership(uint256 _tokenId) public;
}

トークン移行のロジック

function transfer(address _to, uint256 _tokenId) public;

トークン所有者がaddress(送り先)、と_tokenIdを送ってtransfer関数を呼び出す方法

function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;

トークン所有者がapprove関数を呼びたしaddressと_tokenIDを送る、
誰かがtakeOwnership関数を呼び出すことでトークンを受け取る

balanceOf

addressを受け取りそのアドレスのトークン保有量を返す

function balanceOf(address _owner) public view returns (uint256 _balance);

ownerOf

トークンIdを受け取りその所有者のaddressを返す

function ownerOf(uint256 _tokenId) public view returns (address _owner);

safeMath

uintのオーバーフローやアンダーフローを防ぐために使われる

using SafeMath for uint256; 

例えば

function add(uint256 a, uint256 b) internal pure returns (uint256) {
  uint256 c = a + b;
  assert(c >= a);
  return c;
}

assertで c が a よりも大きいことを確認しオーバーフローを防いでいる。

メモ

msg.sender.transfer(msg.value);
で送り返すことも可能

function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) ownerOf(_zombieId) {
    zombies[_zombieId].name = _newName;
}

返り値を配列にする

function sample(address _owner) external view returns (uint[]) {

}

Lesson6

使い方

直接書き込む

index.html
・・・
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
・・・

Infura

高速な読み込みのためのキャッシュレイヤーをもつイーサリアム・ノードのセットを保持するサービス
InfuraをWeb3プロバイダとして使うためには下記のようにセット

var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

Metamask

イーサリウムアカウントと秘密鍵を安全に管理し、Web3.jsのアプリとのやり取りを可能にするChromeとFirefoxの拡張機能

window.addEventListener('load', function() {

  // Web3がブラウザにインジェクトされているかチェック (Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // Mist/MetaMaskのプロバイダの使用
    web3js = new Web3(web3.currentProvider);
  } else {
    // ユーザーがweb3を持たない場合の対処。
    // アプリを使用するためにMetamaskをインストールするよう
    // 伝えるメッセージを表示。
  }

  // アプリのスタート&Web3.jsへの自由なアクセスが可能に:
  startApp()

})

コントラクトアドレスとABI

Web3.jsがコントラクトにアクセするためにはアドレスとABIが必要になる

var myContract = new web3js.eth.Contract(myABI, myContractAddress);

コントラクトアドレス

コントラクトをコンパイルして、イサーリウムにデプロイした際に、永久に有効なイーサリウ上のアドレスのこと

ABI

ABI=Application Binary Interface
コントラクトのメソッドとJSON形式で表しているもの

コントラクトの関数の呼びだし

Call

view関数とpure関数に使われる。ローカルのノードのみで機能し、ブロックチェーンのトランザクションは発生しない。

myContract.methods.myMethod(123).call()
//123をパラメータにもつmyMethodという関数をCall

Send

Sendはトランザクションが発生し、ブロックチェーンを変更させる。

myContract.methods.myMethod(123).send()

実装

index.js
function getZombieDetails(id) {
  //①
  return cryptoZombies.methods.zombies(id).call();
}

// 関数を呼び出し、その結果を処理する:
getZombieDetails(15)
.then(function(result) {
  console.log("Zombie 15: " + JSON.stringify(result));
});

①コントラクトにあるZombie[] public zombiesからzombiesとインデックスidを返す処理
②Web3はPromiseを返すので、promiseがresolveさせるとthenステートメントで続行され、resultをコンソールに出力する。

Metamaskとアカウント

下記のコートでMetamaskのWeb3.jsにインジェクトされたアクティブなアカウントを確認できる

index.js
var userAccount = web3.eth.accounts[0]

ユーザーはいつでもアカウントを切り替えられるため、対応するためには下記のようなコードにする

var accountInterval = setInterval(function() {
  // アカウントが変更されているかチェック
  if (web3.eth.accounts[0] !== userAccount) {
    userAccount = web3.eth.accounts[0];
    // 新アカウントでUIをアップデートするように関数コール
    updateInterface();
  }
}, 100);

トランザクションの送信

「send」には下記が必要になってくる
・関数を呼び出すアカウントのfromアドレス
・ガス
・ブロックチェーン上で有効になるまで時間がかかるため、非同期性を処理するコード

上記を踏まえて、createRandomZombie関数を呼び出す例が下記になる

createRandomZombie.sol
function createRandomZombie(string _name) public {
  require(ownerZombieCount[msg.sender] == 0);
  uint randDna = _generateRandomDna(_name);
  randDna = randDna - randDna % 100;
  _createZombie(_name, randDna);
}
web3.js
function createRandomZombie(name) {
  // しばらく時間がかかるので、UIを更新してユーザーに
  // トランザクションが送信されたことを知らせる
  $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
  // トランザクションをコントラクトに送信する:
  return cryptoZombies.methods.createRandomZombie(name)
  .send({ from: userAccount })
  .on("receipt", function(receipt) {
    $("#txStatus").text("Successfully created " + name + "!");
    // トランザクションがブロックチェーンに取り込まれた。UIをアップデートしよう
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on("error", function(error) {
    // トランザクションが失敗したことをユーザーに通知するために何かを行う
    $("#txStatus").text(error);
  });
}

payable関数の呼び出し

function levelUp(uint _zombieId) external payable {
  require(msg.value == levelUpFee);
  zombies[_zombieId].level++;
}

Web3.jsではいくら送信するかをETHではなくWei(1 ether=10^18 wei)で表示する必要がある

// これが1 ETHをWeiに変換してくれる
web3js.utils.toWei("1", "ether");
web3.js
cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })

イベントのサブスクライブ

event NewZombie(uint zombieId, string name, uint dna);

NewZombieというイベントをサブスクライブするには下記のようにかく

web3.js
// `_to`が`userAccount`と等しいときだけこのコードが動作するように`filter`を使う
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // 現在のユーザーがゾンビを受け取った!
  // それを示すようUIをアップデートする何かを行おう
}).on("error", console.error);

参考サイト

https://cryptozombies.io/jp/

最後に

まだまだ勉強が必要です。

ywzx
エンジニアとして修行中です。特にブロックチェーン に関して勉強をしています。Twitterも始めたのでよければフォローよろしくお願いします。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした