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

概要

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')

Provider

プロバイダ(サービス、リポジトリ、ファクトリ、ヘルパーなど)はコントローラのコンストラクタを介して依存関係を注入することができ、相互にさまざまな関係を作成することが出来ます。 しかし、プロバイダは@Injectableデコレータでインジェクションするだけの単純なクラスだけではありません。プロバイダはビジネスロジックを担当し、コントローラーや他のプロバイダから呼び出されます。

f:id:adrenaline2017:20191130163859p:plain

コントローラはHTTPリクエストを処理し、より複雑なタスクをサービスに任せるものでした。 @Injectableデコレータを持つクラスをNestではプロバイダと認識します。

サービス

サービスの集まりがプロバイダです。単純なCatsServiceプロバイダを作成することから始めましょう。

cats.service.ts

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  //プロパティ
  private readonly cats: Cat[] = [];

  //メソッド
  create(cat: Cat) {
    this.cats.push(cat);
  }

  //メソッド
  findAll(): Cat[] {
    return this.cats;
  }
}

CatsServiceクラスは1つのプロパティと2つのメソッドを持っています。 ここで注目して欲しいのは@Injectableデコレータを使用している所です。 @Injectableデコレータにはメタデータが含まれているので、NestはこのクラスがNestプロバイダであると認識します。 Catインターフェイスを使用していることに注意してください。 すでにサービスクラスが用意されているので、CatsControllerの内部でそれを使用しましょう:

cats.controller.ts

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

CatsServiceは、コントーローラのクラスのコンストラクタを通して注入されます。

依存性注入(DI:Dependency Injection)

オブジェクト指向においてオブジェクト同士が複雑に干渉し合うことは問題視されます。そこでDIコンテナーと言う仕組み(オブジェクト同士の依存関係を橋渡しする為の仕組み)を利用します。DIとは依存性(Dependency)を外から注入(Injection)すると言う意味です。

NestJSでは、TypeScriptの機能のおかげで依存関係をタイプごとに解決しています。そしてこれにより、コントローラのコンストラクタに渡し注入することができるため、依存関係を管理するのは非常に簡単です。コンストラクターの引数型と、登録すみのサービスとを照合して注入すべきオブジェクトを決定する決まりがあります。

@Controller('cats')
export class CatsController {
  //ここで注入
  constructor(private readonly catsService: CatsService) {}

オプションプロバイダ

場合によっては、必ずしも解決する必要のない場合があります。 それは、クラスが構成オブジェクトに依存する場合です。何も渡されなかった場合は、デフォルト値を使用する必要があります。 このような場合、構成するプロバイダが不足してもエラーにならない様にするため、コンストラクシグネチャに@Optional()デコレータを使用します。

import { Injectable, Optional } from '@nestjs/common';

@Injectable()
export class HttpService {
  constructor(
    @Optional() @Inject('HTTP_OPTIONS') private readonly httpClient,
  ) {}
}

スコープ

CatsServiceというものが存在することをモジュールに伝える必要があります。 これを行うには、モジュールファイルapp.module.tsを編集し、サービスを@Module()デコレータのproviders配列に内容を配置します。

app.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  //ここにサービスの内容を記載
  providers: [CatsService],
})
export class ApplicationModule {}

これにより、NestはCatsControllerクラスの依存関係を解決できます。

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

Controller

コントローラ

コントローラの役目は、HTTPリクエストを受け取りレスポンスを返すことです。

コントローラの目的は、アプリケーションの特定の要求を受け取ることです。 ルーティングによりコントローラがどのHTTPリクエストを受け取るかを制御します。下記の図の様に、各コントローラには複数のルートがあり、異なるルートで異なるアクションを実行することが可能です。

f:id:adrenaline2017:20191130163225p:plain

基本的なコントローラを作成するために、クラスとデコレータを使用します。 デコレータはクラスに必要なメタデータに関連付け、Nestがルーティングマップを作成できるようにします。

import { Get, Controller } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  root(): string {
    return this.appService.root();
  }
}

ルーティング

ルーティングはデータをクライアントまで送信するために、コンピュータネットワーク上のデータの経路を決定する制御のことです。

次の例では、@Controllerデコレータを使用して基本的なコントローラの定義をします。@Controller(’cats’)デコレータの'cats'の部分にルートを宣言することで”http://localhost:3000/cats”へのルーティングが定義されます。以下のコマンドを実行する事でcatsコントローラを作成して見ましょう。

$nest g controller cats

cats.controller.ts

import { Controller, Get } from '@nestjs/common';

//catsを追加
@Controller('cats')
export class CatsController {
  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}

@Getデコレータは、対応するリクエストに@Get以下の内容をマップ(関連づける)するよう、Nestに指示します。

リクエストオブジェクト

多くの端末では、クライアントのリクエストの詳細にアクセスする必要があります。Nestではライブラリ固有のリクエストオブジェクトを使用しています。@ Reqデコレータを使用してNestにリクエストオブジェクト(@Req)をハンドラ(@Getの中)に入れることができます。

cats.controller.ts

import { Controller, Get, Req } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request) {
    return 'This action returns all cats';
  }
}

リクエストオブジェクトはHTTPリクエストを表し、クエリ文字列、パラメータ、HTTPヘッダー、および本文のプロパティを持っています。@Bodyや@Queryなどの専用のデコレータを使用することができます。以下は、デコレータとオブジェクトの比較です。

デコレーター オブジェクト
@Request() req
@Response() res
@Next() next
@Session() req.session
@Param(param?: string) req.params / req.params[param]
@Body(param?: string) req.body / req.body[param]
@Query(param?: string) req.query / req.query[param]
@Headers(param?: string) req.headers / req.headers[param]

HTTPリクエストオブジェクト

catsリソースを取得するためのGETリクエストを定義しました。次にPOSTハンドラを作成しましょう:

cats.controller.ts

import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {

  @Post()
  create() {
    return 'This action adds a new cat';
  }

  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}

これらすべては其々のHTTPリクエストメソッドを表します。

メソッド 役割
@Get() リソースの取得
@Post 子リソースの作成、リソースへのデータ追加
@Put() リソースの更新、リソースの作成
@Delete() リソースの削除
@Patch() リソースの一部を更新
@options() リソースがサポートしているメソッドの取得
@Head() リソースのヘッダ (メタデータの取得)

ルートのワイルドカード

パターンベースのルートもサポートされています。 たとえば、アスタリスクワイルドカード(任意の文字を指示するための特殊な文字記号)として使用され、任意の文字の組み合わせに一致します。

@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

上の経路パスは、abcd、ab_cd、abecdなどを表しています。 文字 ? + * ()は正規表現の部分集合です。 ハイフン( - )とドット(。)は、文字列ベースのパスによってそのままの意味で解釈されます。

ステータスコード

レスポンスされるHTTPステータスコードはデフォルトで200ですが、POSTレスポンスでは201以外のものです。ハンドラレベルで@HttpCodeデコレータを追加することで、この動作を簡単に変更できます。

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

多くの場合、ステータスコードは静的ではありませんが、さまざまな要因によって異なります。 その場合、ライブラリ固有のレスポンス(@Resを使用して挿入)オブジェクトを使用できます(エラーの場合は例外をスローします)。

ヘッダー

カスタムレスポンスヘッダを指定するには、@Headerデコレータまたはライブラリ固有のレスポンスオブジェクトを使用できます。

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

ルートパラメータ

URLの一部として動的データを受け入れる必要がある場合、静的パスのルートは役立ちません。 パラメータで経路を定義するために、ルートにおけるルートパラメータを直接的に特定することができます。

@Get(':id')
findOne(@Param() params) {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

特定のパラメータを取得するには、その名前をかっこで渡します。

@Get(':id')
findOne(@Param('id') id) {
  return `This action returns a #${id} cat`;
}

Async/await

現代のJavaScriptのデータの抽出はほとんど非同期で行われます。 そのため、Nestはasync関数をサポートしています。

すべてのasync関数はPromiseを返す必要があります。 これは、Nest自身で解決できる遅延値を返すことができることを意味します。 下の例を見てみましょう。

cats.controller.ts

@Get()
async findAll(): Promise<any[]> {
  return [];
}

上記のコードは有効です。 さらにNestのルートハンドラは、RxJS(イベントのハンドリングや非同期処理をラップするライブラリ)がデータを流れとして返すことによりパワフルに動作します。 Nestは自動的に下のソースを一つずつ取り出し、最初に出力された値を取得します。

cats.controller.ts

@Get()
findAll(): Observable<any[]> {
  return of([]);
}

リクエスペイロード

ペイロード:ヘッダやトレーラなどの付加的情報を除いた、データ本体のこと

前のPOSTルートハンドラ(指定したURLアドレスをリクエストすると、コールバックされる)の例では、クライアントパラメータは受け付けていませんでした。 @Body()引数をここに追加して修正します。

しかし、最初に(TypeScriptを使用する場合)DTOの構造を決定する必要があります。 DTOはデータがネットワークを介してどのように送信されるかを定義するオブジェクトです。 TypeScriptインターフェイスを使用するか、単純なクラスを使用してDTOスキーマを判断できます。 驚いたことに、ここでクラスを使用することをお勧めします。

クラスはJavaScript ES6標準の一部です。したがって、それらは単純な関数を表します。 一方TypeScriptインターフェイスが削除されるため、Nestはそれらを参照できません。 これは、パイプのような機能が変数のメタタイプにアクセスするときに追加の可能性を可能にするため重要なのです。

CreateCatDtoクラスを作成しましょう:

creare-cat.dto.ts

export class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}

基本的なプロパティは3つしかありません。 できるだけ純粋なものとして機能させるように常に努めなければならないので、それらはすべて読み取り専用としてマークされています。 その後、CatsController内で新しく作成したスキーマを使用できます。

cats.controller.ts

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

サンプル

いくつかのデコレータを使用して基本コントローラを作成する例を示します。 次のコントローラは、内部データにアクセスして操作するための2つのメソッドを公開しています。

cats.controller.ts

import { Controller, Get, Post, Body, Put, Param, Delete } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto) {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(@Query() query) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id, @Body() updateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id) {
    return `This action removes a #${id} cat`;
  }
}

エラーの処理

エラー処理については別の章があります。

立ち上げと実行

上記のコントローラはNestはCatsControllerの存在をまだ知りません。ですので、このクラスのインスタンスは作成されません。

コントローラは常にモジュールに属します。なぜなら、@Moduleデコレータ内にコントローラ配列を保持しているからです。 ルートApplicationModule以外のモジュールはないので、これを使ってCatsControllerを導入します:

app.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class ApplicationModule {}

私たちはモジュールクラスにメタデータを添付しました。これでNestはどのコントローラをマウントする必要があるのかを簡単に反映することができます。

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;
}

Module

モジュール

モジュールはコントローラやサービスなどプロパティの集まりで、@Moduleデコレータが付けられたクラスです。 @Moduleデコレータは、Nestがアプリケーション構造を整理するために使用します。

f:id:adrenaline2017:20191130162643p:plain

各アプリケーションには少なくとも1つのルートモジュールがあります。 ルートモジュールは、ツリーの頂点に位置するモジュールです。アプリケーションが小さい場合 、ルートモジュールがアプリケーション内の唯一のモジュールになりますが、大規模なアプリケーションではいくつかのモジュールがあり、それぞれが密接に関連します。

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})

export class AppModule {}
imports このモジュールで必要とするインポートされたモジュールのリスト
controllers 作成しなければならないコントローラ
providers コントローラ以外のクラス(サービスなど)を記載
exports 他のモジュールで利用できるプロバイダの一部

モジュールは、デフォルトでプロバイダをカプセル化します。 つまりこれはモジュールの一部でなく、インポートされたモジュールからエクスポートされていないプロバイダを注入することは出来ないこと意味します。

フィーチャーモジュール(モジュールのimports)

CatsControllerとCatsServiceは同じアプリケーションに属します。 フィーチャモジュールであるCatsModuleに移動します。

cats/cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

ヒント モジュールを作成するには$ nest g module catsコマンドを実行します。 cats.module.tsファイルを定義した後、このモジュールに関連するすべてをcatsディレクトリに移動しました。 最後に、このモジュールをルートモジュール(ApplicationModule)にインポートする必要があります。

app.module.ts

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule {}

ApplicationModuleのほかにCatsModuleも登録することが不可欠になります。

共有モジュール(モジュールのexports)

モジュールは任意のプロバイダの同じインスタンスをモジュール間で簡単に共有できます。

f:id:adrenaline2017:20191130162835p:plain

すべてのモジュールは共有モジュールです。 作成後はどのモジュールでも再利用できます。 他のいくつかのモジュール間でCatsServiceインスタンスを共有したいと考えてみましょう。 そのためには、以下のようにCatsServiceをexporotsで配置する必要があります。

cats.module.ts JS

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

CatsModuleをインポートする各モジュールは、CatsServiceへのアクセス権を持ちます。このモジュールをインポートする全てのモジュールと同じインスタンスを共有します。

モジュールの再エクスポート

モジュールは内部プロバイダをエクスポートできます。 さらに、彼ら自身がインポートしたモジュールをエクスポートすることもできます。

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

依存性注入(DI)

モジュールクラスは、プロバイダを注入することもできます:

cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  //ここでプロバイダを注入
  constructor(private readonly catsService: CatsService) {}
}

ただし、モジュールクラスはプロバイダによってインジェクトできません。

グローバルモジュール

同じモジュールをどこにでもインポートする必要がある場合は、グローバルモジュールにする必要があります。 Angularでは、プロバイダはグローバルスコープに登録されています。なので定義されると、どこでも利用できます。 一方、Nestはモジュールスコープ内のプロバイダをカプセル化されます。 モジュールのプロバイダをインポートせずに他の場所で使用することはできません。 しかし、時には、ヘルパー、データベース接続など、いつでもすぐに利用できる様にしたいこともあります。 そのために、モジュールを@Global()デコレータでグローバルなものにすることができます。

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

@Global()デコレータは、モジュールをグローバルスコープにします。 グローバルモジュールは、ルートモジュールまたはコアモジュールによって1回だけ登録することが望ましいです。

ヒント 単純に全てをグローバルにすることは良いことではありません。 グローバルモジュールは、必要な定型文の量を減らすために利用できます。 インポート配列は、モジュールAPIを透過的にする最も良い方法です。

ダイナミックモジュール

Nestのモジュールシステムには、動的モジュールと呼ばれる機能があります。 これは、努力せずにカスタマイズ可能なモジュールを作成することができます。 DatabaseModuleを見てみましょう:

import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
  providers: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

ヒント forRoot()は、動的に同期または非同期のいずれかのモジュールを返します(Promise)。

このモジュールは、デフォルトでConnectionプロバイダを定義しますが、渡されたオプションやエンティティに応じて、リポジトリなどのプロバイダのコレクションを公開します。 実際、動的モジュールは基本モジュールのメタデータを拡張します。 この実質的な機能は、プロバイダを動的に登録する必要がある場合に便利です。 次に、次のようにDatabaseModuleをインポートできます。

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [
    DatabaseModule.forRoot([User]),
  ],
})
export class ApplicationModule {}

動的モジュールをエクスポートするには、関数呼び出し部分を省略することができます。

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [
    DatabaseModule.forRoot([User]),
  ],
  exports: [DatabaseModule]
})
export class ApplicationModule {}