20
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Ethereum] Chainlinkで阿部寛の最新の映画出演情報をスマートコントラクトに記録する

Last updated at Posted at 2020-09-09

分散型オラクルであるChainlinkを使い、WEBサイトからスクレイピングしたデータをスマートコントラクトに記録しみようと思います。
取得するデータは、最速最強のWebサイトとして有名な阿部寛の最新映画出演情報です。
Chainlinkを使用するにあたってはスマートコントラクトを書くだけでなく、NodeやExternal Adapter、JOB(v2)も実際に動かしてみます。

阿部寛のホームページ.png

Chainlinkとは

Ethereumでは、スマートコントラクト内から外部データにアクセスできません。
Chainlinkは、外部にあるデータをスマートコントラクト内部に保存するためのミドルウェアで、スマートコントラクト内から外部データにアクセスする仕組みを提供しており、分散型オラクルと呼ばれています。
最近話題のDeFiでは、スマートコントラクトから外部のトークンやETHのレート情報にアクセスする際に使われています。

本件概要

阿部寛の最新映画出演情報を取得する流れは、以下の通りです。

  1. WEBスクレイピングコントラクト(WebScrapingData.sol)にデータをリクエストする。
  2. Oracleコントラクトを経由して、Chainlink Nodeに登録されたJobが実行される。
  3. JobがWebスクレイピングを行うExternal Adapterを呼び出す。
  4. External Adapterが阿部寛のWebサイトをスクレイピングし、最新の映画出演情報を取得する。
  5. External Adapterよりデータを取得したJobがOracleコントラクトを経由してWEBスクレイピングコントラクトにデータを書き込む。

全画面_2022_05_22_23_21.png

No 名称 役割 処理概要
1 WebScrapingData.sol 外部データ取得のリクエストの受付と、保存を行うスマートコントラクト 阿部寛の最新映画出演情報の取得をリクエスト、管理する
2 Oracle.sol Chainlink NodeとWebScrapingData.sol間のリクエストを橋渡しするコントラクト
3 JOB コントラクトからのリクエストの受付や、外部データとのやり取りなど一連の処理を実行する仕組み External Adapterを呼び出し、受け取った値をbytes32でコントラクトに書き込む
4 External Adapter 外部データを取得し、Chanlinkで扱える形にしてデータをChainlink Nodeに渡すサービス 阿部寛のWebsサイトをスクレイピングして最新映画出演情報を取得する。

実装してみる

1. External Adapterを動かす

Extarnal Adapterは指定のフォーマットのインターフェースを持っていればどんな言語で開発しても問題ありません。
具体的には下記の2点の要件を満たす必要があります。

  1. POST APIが、処理成功時に{jobRunID: string, data: any}の形でデータを返却すること。
  2. POST APIが、処理失敗時に{jobRunID: string, status: string, error: any}の形でデータを返却すること。

External Adapterの開発方法は、下記公式ドキュメントにも記載されています。
https://docs.chain.link/docs/developers

今回はWebスクレイピングをし、指定したhtml要素の値を取得する処理を実装します。
汎用的に使用できるAdapterにするため、引数にid, url, path, filterを指定できるようにします。

Input Params

Key Description Value Example
id Required chainlink nodeに渡すためのjobRunID 1
url Required スクレイピングする対象のURL http://abehiroshi.la.coocan.jp/movie/eiga.htm
path Required データを取得する対象のDOMツリー html.body.center.1.table.tr
filter Optional 取得したDOMツリーをフィルタするパラメーター td.strong

Output Format

{
    "jobRunID": "1",
    "data": [
        "2022年夏公開予定",
        "「",
        {
            "type": "a",
            "content": [
                "異動辞令は音楽隊!"
            ],
            "attributes": {
                "href": "https://gaga.ne.jp/ongakutai/",
                "target": "_blank"
            }
        },
        "」"
    ]
}

実際にNest.js(Node.js)で実装したものがこちら。
chainlink-scraping-adapter

下記コマンドで実行できます。

$ git clone git@github.com:biga816/chainlink-scraping-adapter.git
$ npm install
$ npm run start

下記のような実行結果になれば正常に動作しています。
Postman.png

2. Chainlink Nodeを動かす

Chainlink Node、PostgreSQL、External Adapterの3つを同時に動かす必要があるため、Docker Composeを使用します。

フォルダ構成は以下の通りにした上で、docker-compose.ymlを作成します。
今回はRinkeby用の設定になります。

.
├── chainlink-node
└── chainlink-scraping-adapter
./chainlink-node/docker-compose.yml
version: '3.4'

services:
  postgresql:
    image: postgres:14.2
    container_name: postgresql
    ports:
      - 5432:5432
    volumes:
      - ./postgres/init:/docker-entrypoint-initdb.d
      - database:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: root
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
      POSTGRES_HOST_AUTH_METHOD: password
    hostname: postgres
    restart: always
    user: root

  chainlink:
    image: smartcontract/chainlink:1.3.0
    container_name: chainlink
    tty: true
    ports:
      - 6688:6688
    env_file:
      - ./chainlink/.env
    volumes:
      - ./chainlink/data:/chainlink
    depends_on:
      - postgresql
    entrypoint: "/bin/bash -c 'chainlink local n -p /chainlink/.password -a /chainlink/.api'"
    restart: on-failure

  adapter:
    container_name: scraping-adapter
    ports:
      - "5000:5000"
    build:
      context: ../chainlink-scraping-adapter
      dockerfile: ./Dockerfile
    image: scraping-adapter
    restart: on-failure

volumes:
  database:
    driver: local

次に、それぞれのサービスに必要な設定ファイルを追加していきます。
IDやパスワードなどは適宜書き換えてください。

./chainlink-nodepostgres/init/1_create.sql
CREATE DATABASE chainlink;
./chainlink-node/postgres/chainlink/.env
ROOT=/chainlink
LOG_LEVEL=debug
ETH_CHAIN_ID=4
MIN_OUTGOING_CONFIRMATIONS=2
LINK_CONTRACT_ADDRESS=0x01BE23585060835E02B77ef475b0Cc51aA1e0709
CHAINLINK_TLS_PORT=0
SECURE_COOKIES=false
ALLOW_ORIGINS=*
ETH_URL=wss://rinkeby.infura.io/ws/v3/<YOUR INFURA ID>
DATABASE_URL=postgresql://root:root@postgresql:5432/chainlink?sslmode=disable
DATABASE_TIMEOUT=0
./chainlink-node/chainlink/data/.password
<YOUR WALLET PASSWORD>
./chainlink-node/chainlink/data/.api
<YOUR LOGIN EMAIL>
<YOUR LOGIN PASSWORD>

ここまでできたら準備完了です。
下記コマンドで、chainlink nodeを立ち上げます。

$ cd chainlink-node
$ docker-compose up -d

下記URLにアクセスしてsignin画面が表示されれば無事起動完了です。
http://localhost:6688
Sign_In.png

3. Webスクレイピング用のコントラクトを作成する

ChsinlinkのコントラクトとOpenZeppelinのコントラクトを利用するため、まずnpmモジュールをインストールします。

$ cd chainlink-node
$ npm install --save @chainlink/contracts openzeppelin-solidity

次にWebスクレイピングの実行をリクエストし、実行結果を保存するためのコントラクトWebScrapingData.solを作成します。

./chainlink-node/contracts/WebScrapingData.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.7.0;

import "@chainlink/contracts/src/v0.7/ChainlinkClient.sol";
import "openzeppelin-solidity/contracts/access/Ownable.sol";

contract WebScrapingData is ChainlinkClient, Ownable {
    using Chainlink for Chainlink.Request;

    struct Response {
        string date;
        string title;
    }

    string public url;
    Response public response;

    constructor(address _link) public Ownable() {
        if (_link == address(0)) {
            setPublicChainlinkToken();
        } else {
            setChainlinkToken(_link);
        }
    }

    function setUrl(string memory _url) public onlyOwner {
        url = _url;
    }

    function getChainlinkToken() public view returns (address) {
        return chainlinkTokenAddress();
    }

    function createRequestTo(
        address _oracle,
        bytes32 _jobId,
        uint256 _payment,
        string memory _path,
        string memory _filter
    ) public onlyOwner returns (bytes32 requestId) {
        require(bytes(url).length > 0, "url is required");
        Chainlink.Request memory req = buildChainlinkRequest(
            _jobId,
            address(this),
            this.fulfill.selector
        );
        req.add("url", url);
        req.add("path", _path);
        req.add("filter", _filter);
        requestId = sendChainlinkRequestTo(_oracle, req, _payment);
    }

    function fulfill(
        bytes32 _requestId,
        string calldata _date,
        string calldata _title
    ) public recordChainlinkFulfillment(_requestId) {
        response = Response({date: _date, title: _title});
    }

    /**
     * @notice Allows the owner to withdraw any LINK balance on the contract
     */
    function withdrawLink() public onlyOwner {
        LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());
        require(
            link.transfer(msg.sender, link.balanceOf(address(this))),
            "Unable to transfer"
        );
    }

    function cancelRequest(
        bytes32 _requestId,
        uint256 _payment,
        bytes4 _callbackFunctionId,
        uint256 _expiration
    ) public onlyOwner {
        cancelChainlinkRequest(
            _requestId,
            _payment,
            _callbackFunctionId,
            _expiration
        );
    }
}

createRequestTo()がデータの取得をリクエストするファンクションで、fulfill()がデータを受け取るためのファンクションになります。
JOBのv2からはfulfillメソッドにて複数のデータを受け取ることができるようになったので、ここでは最新映画出演情報のうち、公開日とタイトルを受け取っています。

4. Oracleコントラクトを作成する。

fulfillメソッドにて複数のデータを受け取るには、ChainlinkのコントラクトにあるOracle.soldではなくOperator.solを使って下記のコントラクトを作成する必要があります。

solidity./chainlink-node/contracts/Oracle.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "@chainlink/contracts/src/v0.7/Operator.sol";

contract Oracle is Operator {
    constructor(address _link, address owner) public Operator(_link, owner) {}
}

5. コントラクトをデプロイする。

Truffleを使用する場合は以下のようなmigrationファイルを用意することでWebScrapingDataとOracleの2つのコントラクトをデプロイします。

./chainlink-node/migrations/2_webscrapingdata_migration.js
require("dotenv/config");
const WebScrapingData = artifacts.require("WebScrapingData");
const Oracle = artifacts.require("Oracle");

module.exports = (deployer, network, [defaultAccount]) => {
  Oracle.setProvider(deployer.provider);

  deployer
    .deploy(WebScrapingData, process.env.LINKTOKEN_ADDRESS)
    .then(async (instance) => {
      await instance.setUrl("http://abehiroshi.la.coocan.jp/movie/eiga.htm");
      Oracle.setProvider(deployer.provider);
      return deployer.deploy(
        Oracle,
        process.env.LINKTOKEN_ADDRESS,
        defaultAccount,
        {
          from: defaultAccount,
        }
      );
    })
    .then(async (instance) => {
      await instance.setAuthorizedSenders(
        [process.env.CHAINLINK_NODE_ACCOUNT],
        { from: defaultAccount }
      );
    });
};

※1 WebScrapingDataとOracleそれぞれのデプロイ時にLINKトークンのアドレスを設定する必要があるため、環境変数LINKTOKEN_ADDRESSにはRinkebyのLINKトークンのアドレスである0x01BE23585060835E02B77ef475b0Cc51aA1e0709をセットする必要があります。

※2 OracleコントラクトはChainlink Nodeに対して権限を与える必要があるため、デプロイ後にsetAuthorizedSenders()を用いてChainlink Nodeのアカウントアドレスをセットします。Chainlink Nodeのアカウントアドレスは下記のトップページで確認できます。

Bridges.png

ここまでに作成した構成は以下のリポジトに格納してあるので、うまく動かない場合はこちらをご利用ください。
https://github.com/biga816/chainlink-node

6. Bridgeを登録する

作成したExternal Adapterを呼び出すために、下記画面よりBridgeを登録します。
Bridges.png

各項目に入力する値は以下の通りです。

項目名
Bridge Name scraping
Bridge URL http://adapter:5000
Minimum Contract Payment 0
Confirmations 0

7. JOBを登録する

下記画面よりJOBを登録します。
Jobs.png

JOBの内容は以下の通りです。
contractAddressには先ほどデプロイしたOracleコントラクトのアドレスをセットします。

type                = "directrequest"
schemaVersion       = 1
name                = "Get Scraping Data"
contractAddress     = "<YOUR_ORACLE_ADDRESS>"
observationSource   = """
  decode_log [
    type="ethabidecodelog"
    abi="OracleRequest(bytes32 indexed specId, address requester, bytes32 requestId, uint256 payment, address callbackAddr, bytes4 callbackFunctionId, uint256 cancelExpiration, uint256 dataVersion, bytes data)"
    data="$(jobRun.logData)"
    topics="$(jobRun.logTopics)"
  ]
  decode_cbor [type=cborparse data="$(decode_log.data)"]
  fetch [
    type="bridge"
    name="scraping"
    requestData="{\\"id\\": $(jobSpec.externalJobID), \\"url\\": $(decode_cbor.url), \\"path\\": $(decode_cbor.path), \\"filter\\": $(decode_cbor.filter)}"
  ]

  decode_log -> decode_cbor -> fetch

  data_date [type="jsonparse" path="data,0" data="$(fetch)"]
  date_title [type="jsonparse" path="data,2,content,0" data="$(fetch)"]

  fetch -> data_date
  fetch -> date_title

  encode_data [
    type="ethabiencode"
    abi="(bytes32 requestId, string date, string title)"
    data="{\\"requestId\\": $(decode_log.requestId), \\"date\\": $(data_date), \\"title\\": $(date_title)}"
  ]
  
  data_date -> encode_data
  date_title -> encode_data
  
  encode_tx [
    type="ethabiencode"
    abi="fulfillOracleRequest2(bytes32 requestId, uint256 payment, address callbackAddress, bytes4 callbackFunctionId, uint256 expiration, bytes calldata data)"
    data="{\\"requestId\\": $(decode_log.requestId), \\"payment\\": $(decode_log.payment), \\"callbackAddress\\": $(decode_log.callbackAddr), \\"callbackFunctionId\\": $(decode_log.callbackFunctionId), \\"expiration\\": $(decode_log.cancelExpiration), \\"data\\": $(encode_data)}"
  ]
  submit_tx [
    type="ethtx" to="<YOUR_ORACLE_ADDRESS>"
    data="$(encode_tx)"
  ]

  encode_data -> encode_tx -> submit_tx
"""

各taskの役割は以下の通りです。

type Description
ethabidecodelog コントラクトから受け取ったデータをデコードする
bridge 指定したbridgeを呼び出す
jsonparse jsonデータをパースする
ethabiencode コントラクトを呼べる形にデータをエンコードする
ethtx コントラクトを実行する

ethabiencodeのabiに指定しているメソッド名がfulfillOracleRequest2になっているのがポイントです。
複数のデータをコントラクトに返したい場合には、fulfillOracleRequestではなくfulfillOracleRequest2にする必要があります。

上記以外のtaskを使用したい場合は、こちらのドキュメントから探せます。
https://docs.chain.link/docs/tasks/

8.LINKトークン、ETHを準備する

コントラクト実行にあたり、WebScrapingDataコントラクトにはLINKトークン、Chainlink NodeのアカウントアドレスにはETHが必要なので、コントラクト実行前にそれぞれ送ります。
RinkebyのLinkトークンは、ここから取得できます。
https://faucets.chain.link/

JOBの登録が完了すると、下記のようなフローが確認できます。
Sign_In.png

9. コントラクトを実行する。

コントラクトの実行には、Remixを使用します。
Remixを開いたら、下記手順でコントラクトを読み込みます。

  1. FILE EXPLORERSで先ほどデプロイしたWebScrapingDataコントラクトのABIをファイルとして保存する。
  2. DEPLOY & RUN TRANSACTIONSでENVIRONMENTInjected Web3を選択する。(コントラクトのでデプロイ時のアドレスと同じであること。)
  3. CONTRACTAt Addressに先ほどデプロイしたWebScrapingDataのコントラクトアドレスを入力し、At Addressを押下する。

Remix_-_Ethereum_IDE.png

コントラクトが読み込めたら、urlファンクションを実行します。
デプロイ時にセットしたhttp://abehiroshi.la.coocan.jp/movie/eiga.htmが返ってくれば準備OKです。
Remix_-_Ethereum_IDE.png

最後に、createRequestToファンクションに下記値をセットして呼び出します。

項目名
_oracle Oracleコントラクトのアドレス
_jobId 作成したJOBのIDをbyte32に変換したもの※1
_payment 支払うLINKトークンの数量 (Decimalsは18)
_path External Adapterに渡すpath
_filter External Adapterに渡すpath

※1
こちらで変換できます。
https://web3-type-converter.brn.sh/
変換の際には、実際のJOBIDからハイフンを削除したものをbyte32に変換する必要があります。
例)
010e6657-4114-4248-8737-3272de4d9c2a -> 010e66574114424887373272de4d9c2a -> 0x3031306536363537343131343432343838373337333237326465346439633261

Remix_-_Ethereum_IDE.png

トランザクションが確定すると、JOBが動き出します。
Jobs.png

JOBがCompletedになったら、Remixに戻り、responseファンクションを実行します。
値が格納されていたら、データの取得完了です。
Remix_-_Ethereum_IDE.png

ちなみにこちらが阿部寛の最新映画出演情報です。
一番上の最新映画出演情報がコントラクトに格納されたものと同じであることが分かります。
阿部_寛の映画出演.png

まとめ

これでChainlinkの機能を全体的に使用した上で、阿部寛のアクティブな最新映画出演情報をスマートコントラクトに記録することができました。
今回は阿部寛のWebサイトのデータを対象としましたが、作成したExternal Adapterは汎用的にWEBスクレイピングできるように作成したため、他のサイトからもデータをとってこれますし、そもそもExternal Adapterを使わなくてもJOBを正しく定義することで様々な外部データをコントラクトに記録することができます。
今後さらに面白い用途に応用していけたらと思います。

20
11
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?