シンプルなDAppを手元で試してテストネットまでデプロイしてみる【社内ハッカソン】

5/14に久しぶりに社内ハッカソンがありました。 コロナなどの影響で2年ほど見送りが続いていたんですが、 運営委員を再編するところから小規模ながらようやく開催できました。

ハッカソンといっても各自好きなこと・目標を決めて1日集中して何か成果物を作るみたいな感じなので、 今回は前から気になっていたDApps/ブロックチェーン周りの技術に触れてみることにしました。

もし、自分も気になっているな〜という方がいたら同じ手順を踏んでもらえると感触をつかめるかもしれません。

今回の記事についての注意点: 筆者の理解がまだ浅いことと、1日の中での調査のため、正しくない説明や記述がある可能性があります。

今回の目標

  • Web上で公開されているシンプルなアプリをCloneしてきて内部構造やContract、front-endのコードを読む
  • そのアプリを手元でコンパイル、ローカルのネットワークに接続してトランザクションなどの動きをみる
  • シンプルなアプリを修正できるところがあれば簡単な修正・カスタマイズを加えてみる
  • アプリをテストネットワークに繋いでそこでトランザクションを実行してみる(front-endはローカルで起動)

当日までに済ませていたこと・事前準備

以前から少しだけブロックチェーン・暗号通貨周りのあれこれを試したりしてましたが、 それに加えて簡単な勉強などを事前準備として行っていました。

MetaMaskのインストールや暗号通貨の送金など

この辺は2018年のCyptoKittiesで遊んでみた記事でやってました。

なんか技術書読む

これもだいぶ前だったと思うのですが、とりあえず以下の書籍を読みました。 全体像がつかめてよかったです。

Axie Infinityやってみる

去年だいぶ話題だったAxie Infinityをやってみました。 CyptoKittiesの時よりも、ブロックチェーン周りのエコシステム・Play to Earnみたいなところを体感できたきがします。

axieinfinity.com

CryptoZombiesでSolidityの勉強

Solidityの勉強のためにCryptoZombiesのプログラムを途中までやりました。 CryptoZombiesすごいわかりやすいし楽しいです。

cryptozombies.io

TODOアプリ起動確認・ローカルネット確認編

とりえあず、当日の流れやポイントを紹介していきます。

当日の前半戦は、以下の記事を参考にさせていただきました。 とてもわかりやすいハンズオン記事感謝です。 zenn.dev

サンプルアプリのリポジトリも公開されているので、実装手順は後日ゆっくり試してみることにして、 コードと構造を確認後、完成形を動かすところからやってみます。

nodeとかをinstallするのは省略です。

とりあえずパッケージinstallします。

$ yarn install

つぎに早速コントラクトをコンパイルしてみます。

$ npx hardhat compile
Compiling 1 file with 0.8.4
Generating typings for: 1 artifacts in dir: typechain for target: ethers-v5
Successfully generated 5 typings!
Compilation finished successfully

無事とおりました。

成果物がartifacts以下に出力されます。

$ ls artifacts/contracts/TodoList.sol
TodoList.dbg.json   TodoList.json

出力されたTodoList.json はABI(Application Binary Interface)と呼ばれ、 front側から呼び出す時のインターフェースになるっぽいです。

次にローカルのネットワークにノード立てます。

$ npx hardhat node
npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
...

無事ネットワークが立ちました。 初期のアカウントのアドレスと秘密鍵が20個表示されてます。

ネットワークはhttp://127.0.0.1:8545/をエントリーにいろいろ通信できるみたいです。 立ち上げたコンソールはそのままにしておきます。

さて、ローカルのネットワークはたちましたが、特に何も動いてないので 先ほどコンパイルしてみたコントラクトをデプロイします。

$ npx hardhat run scripts/deploy.ts --network localhost
localhost
No need to generate any newer typings.
todoList deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3

デプロイできました。 デプロイ後コントラクトのアドレスが表示されます。 このアドレスはweb-front側で利用しています。

このとき、立てっぱなしにしていたネットワークのコンソール側にもログがでます。

web3_clientVersion (2)
eth_chainId
eth_accounts
eth_blockNumber
eth_chainId (2)
eth_estimateGas
eth_getBlockByNumber
eth_feeHistory
eth_sendTransaction
 Contract deployment: TodoList
 Contract address:  0x5fbdb2315678afecb367f032d93f642f64180aa3
 Transaction:     0x376862b3b1bc5fa66710ca238c5ed4f1189c05d46f9a09058b1fffa2ee3b7bb4
 From:        0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
 Value:        0 ETH
 Gas used:      631868 of 631868
 Block #1:      0xf416cd7e33b9960166ece22b4c936aecad62c02888f7fcba585ce7dce76491a4

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt

次にfront側たてます。 web3.jsが使われるのかなーとおもっていましたが、最近はethers.jsが主流なんですかね? web-frontディレクトリに移動してfront側のパッケージをinstallします。

$ cd webfront
$ yarn install

frontを立ち上げます。

$ yarn dev

いい感じに立ち上がりました! 実際にCreate Taskなどを押すと、ネットワーク側のトランザクションのログなどが流れるのを確認できます! うーんいいですね。

Web3Providerへの変更とconnect編

さて、いったん動きを確認できましたが、ここで疑問がでました。 そもそもコントラクトとしてはviewだけなら特にgas代はかからないが、 書き込みのTransactionはgas代がかかるはず...そもそもMetaMaskなどやアカウントとの連携はどうするんだ? というとこです。

これはApp.tsxのproviderを取得しているところの修正が必要なのかなと。

export const App: VFC = () => {
  const provider = new ethers.providers.JsonRpcProvider();
  const signer = provider.getSigner();
  const contract = new ethers.Contract(contractAddress, artifact.abi, provider);
  const contractWithSigner = contract.connect(signer);

JsonRpcProviderはデフォルトではhttp://localhost:8545につながりにいくんで、 そこでローカルのネットワークに接続して、getSignerでindex 0のAccount #0: 0xf39fd6...を引っ張ってくる。 これはわかるが、なんかノーコストでトランザクション動かせるのはなんでなんだろ? ここはちょっとよくわからなかった。

ひとまず、テストネットではこうはいかないと思うのでWeb3Providerに変えてみます。

const provider = new ethers.providers.Web3Provider(window.ethereum);

これでMetaMaskと連携できるはず。

試す前にMetaMaskをローカルネットに接続しておきます。 今回は元々MetaMaskで利用していたアカウントを使ってみます。 MetaMaskのネットワーク切り替えメニューの下部にネットワークを追加というメニューがあるのでそこから追加します。

RPC URLは先ほどローカルネットワーク立てたときに表示されたやつですね。 チェーンIDはどこで確認するかわかんなかったんですが、URLなどを入力したら自動で入りました。 多分定型で31337っぽいです。 通貨記号はテスト用はGOと設定するのが良いっぽいのでそうしました。

これでMetaMaskにローカルネットワーク追加できました。

このアカウントの残高はローカルネットワークでは0(GO)です。 このままでは検証ができないので、以下を参考に初期アカウントから送金するのを試してみました。

zenn.dev

以下をscripts/sendGasFee.tsを作成します。

import { ethers } from "hardhat";

async function main() {
  const transactionSend = {
    to: "0x.....[送信先アカウントアドレス]",
    value: ethers.utils.parseEther("10.0"),
  };

  const [account] = await ethers.getSigners();
  await account.sendTransaction(transactionSend);
  console.log("success");
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

以下を実行します。

$ npx hardhat run scripts/sendGasFee.ts --network localhost

送金できてますね!

さて、これでもうちょい本格的に試せると思ったんですが、ちょっと問題が。 web-frontのページにアクセスしてもMetaMaskが反応しないんですよね。 MetaMaskの左側は未接続と表記が。 確認すると接続ボタンを押してくださいとでます。

なるほど。確かに各種サービスにそういうボタンあります。 ということで以下を参考させていただき修正してみました。

developers.gmo.jp

以下を呼び出せばいけそうです。

const account = await window.ethereum.request({ method: 'eth_requestAccounts' });

とりあえず、コードを修正して呼びだすようにして、無事接続できました。

とりあえず、接続したことでコントラクトからのデータ取得も再度正常にできるようになりました。 この状態で再度トランザクションを実行してみます。

書き込みを実行するとMetaMaskが起動します! gas代も表示されます。それっぽいです!

MetaMaskの確認を承認すると、TODOリストにTaskが書き込まれ、実際にアカウントの通貨量もgas代として減ってます。

しかし、ネットワークのログを見ると以下のログがでていました。

Error: Transaction reverted: function selector was not recognized and there's no fallback function
....

これはTodoList.sol側にrecieveとfallbackのメソッドを実装したら出なくなりました。 techblg.app

ただ、コントラクト側に変更を加えて再度デプロイしたら別アドレスになってしまい、 storage値なども引き継がれてないので、どうマイグレートするといいのか、他のデプロイ方法があるのかはちょっと調べられませんでした。 今回はテストなので問題ないですが。

Counterアプリ起動確認編

この辺りで当日の成果物どうしようかなという感じに。 一旦全般の流れは確認できたのですが、 いかんせん自分がreactとかに詳しくないこともあり、 もっとシンプルなアプリの方が、なんか弄りやすそうな気がしてきました。 あと、デモ的にもそっちのほうがウケがよいかなと。

ということで、後半はこちらのカウンターアプリの手順を追うことに(こちらも記事公開ありがとうございます)。 zenn.dev

connectボタンの処理も実装済みです。 実はfront側にコントラクトeventのコールバックも実装してあるので、 トランザクション完了のタイミングなどを正確に取得して画面更新などが走ります。

手順を追う部分は省略しますが、 こちらも公開されているリポジトリからパッケージインストール、コントラクトコンパイル、デプロイ、フロント起動という順で試しました。

また、先ほどのTODOアプリの同様にトランザクション時にエラーがでたので、recieveとfallbackのメソッドを実装しました。 さらに、今度は既存アカウントに送金するのではなく、 記事の手順通りローカルで発行されたアカウントをMetaMaskにインポートすることで残高がある状態にしてみました。

テストネットデプロイ編

2つのサンプルアプリを触ってだいぶ理解も進んできたので、当初の目標であるテストネットワークにデプロイするところを目指すことに。 これでデモ的にもハクがつくでしょう。 Ethereumのテストネットワークの一つであるRopstenにデプロイしてみます。

まずはですが、先ほどと同様にMetaMaskの接続先をRopstenネットワークに切り替えます。 なんか選択肢に出てこないなーとおもったらMetaMaskの設定->高度な設定にテストネットワークを表示するオプションがありました。

このオプションを有効にすると普通にネットワーク切り替えにでてきました。

次にテスト用のEtherをFaucetに要求します。こちらを参考にしています。

zenn.dev

最初どこからだ?とまよったんですが、 Ropstenネットワークに切り替えた状態で、 Metamaskの「購入ボタン」を押すとでるメニューの下部スクロールしたところにありました。

その先に進んだrequest 1 ether from faucetボタン押すとトランザクションを発行できます。 テストネットワークといえメインネット同じPoWでコンセンサスアルゴリズムが走るので、 完了まで1~2分ほど若干時間がかかります。

ちなみfaucetのページにtransactionのリンクが表示されるので、etherscanで内容確認できます。

次は、実際にRopstenネットワークにデプロイします。 デプロイのためにINFURAに登録します。 infura.io

プロジェクトの情報を入力したら設定画面にて、 ENDPOINTSの項目でRopstenを選択しhttps://ropsten.infura.io/v3/〜の部分をメモります。

さらに、サンプルのカウンターアプリのpackages/contract/hardhat.config.jsを以下のように修正します。 アカウントのPrivateKeyはMetaMaskのアカウントの詳細->秘密鍵のエクスポートから取得できます。 絶対に流出(どこかに載せたり、リポジトリにコミットしたり)しないようにしましょう。

require("@nomiclabs/hardhat-waffle");

module.exports = {
  solidity: "0.8.0",
  networks: {
    ropsten: {
      url: "https://ropsten.infura.io/v3/[メモしたENDPOINTS URL]",
      accounts: ["MetaMaskで管理してるアカウントのPrivateKey"],
    }
  }
};

これで準備は整いました。 ネットワークを指定してデプロイします。

$ npx hardhat run --network ropsten scripts/deploy.js
deployed to: 0x36d7A108c6877Fa15D31275e890C9706452ADE51

デプロイが完了すると、gas代としてアカウントのETHがちゃんと減ってます。

またINFURA側のダッシュボードにもアクセスあることが確認できます。

よし、、、 ということで、本日の成果物、 gas代を払って数字を上げ下げする、その名も「Super Counter」が完成しました。(ほとんどコーディングはしてない)

なお、検証が目的なのでfrontはローカルで立てるだけでどこにもデプロイしてません。

まとめ

久しぶりのハッカソンたのしかったです! DApps/スマートコントラクトという概念に触れるのは、ちゃんと動くアプリケーションのレベルでは初だったので新鮮でした。 バックエンドにRDBのようなものが一切ないのに値が保存されているのは、なんか感慨深いですね。 新しいパラダイムに触れるのはプログラムを始めた時から、いつになってもワクワクします。

世間ではNFTとかもろもろ、投資、投機的な側面が強調されていいるような印象もあり、 記事を検索しても技術記事と混在しているような感じもします。

いわゆるweb3と呼ばれるサービスやシステムが、 既存の集中管理型のサービスの置き換えになるわけでもなく、 front-endが必要ないわけでもなく、 技術的な側面を見ると本質的にはどういう優位性があるのか?と疑心暗鬼になる気持ちもわかります。

ただ、アプリケーションやトークンが外部経済との接続を持つという点だったり(投機的目線の理由である通り)、 トランザクションの透明性だったり、 その上でそれらの手続きが自動実行可能など、 新しくできることの可能性が増えたのだなという感じがします。

どこかでweb1はread、web2はread-write、web3はread-write-ownと聞いて、 なるほどなーと思ったのですが、 それも踏まえてweb3やDappsは「価値」を扱っていくのが本質なのかもとも感じました。 人やコミュニティに応じて色々な価値基準がありますが、そいういうがいろいろ繋がってくんのかなというか。

と、ちょっとまとめでわかった風に書きましたが、 まだまだ理解が足りてませんので、引き続きいろいろ勉強したいと思います。 本質を理解してこれらのパラダイムで何か新しいものを生み出してみたいですね。