Web3 とは、なんだったのか?概念ではなく体験して理解する〜Dapps開発編〜

前回の記事では、暗号通貨(ERC20)、NFT(ERC721)、NFTパッケージ(ERC1155)の現実にデプロイされているスマートコントラクトを覗いて、さらにテストネットに自作のスマートコントラクトをデプロイしました。

今回も、同様にテストネットを使いますので、お金を使うことはありません。ERC20 をデプロイしたことのない読者の方は、前回の記事を読んでみてください。この記事でも詳細は省いておりますが、振り返りをしますので、開発者で理解の良い方はこの記事だけでも目的を達成できるようになっています。

Web3 とは、なんだったのか?概念ではなく具体で理解する〜超入門〜

Web3 とは、なんだったのか?概念ではなく体験して理解する〜超入門〜

web3js を用いて、Mumbai テストネット上に暗号通貨(ERC20トークン)を送り合うピアボーナスアプリをデプロイします。

今回は、次のようなことをします。作成したアプリは公開されています。

アプリとコードの公開先

ゴール:OkojoArigato という感謝の気持ちとERC20トークンを送り合うスマートコントラクトをデプロイして、Okojo Coin を送り合うDappsを作る

今回、作成したアプリケーションは、https://okojo-arigato.web.app で公開しています。Mumbai テストネットで Mumbai MATIC を持っていることと、ERC20トークンである OKJC を持っている必要があります。テストで利用したい方は、OKJC を送金するので、会社のお問い合わせか、@shr_f までご連絡ください。

  • Remix IDE から、web3.js のAPIを使ってみる
  • ABI(Application Binary Interface) とスマートコントラクトのアドレスからERC20スマートコントラクト(Okojo Coin)のメソッドを呼び出してみる
  • ERC20 スマートコントラクトの暗号通貨と同時に感謝の言葉を伝えるスマートコントラクト(OkojoArigato)をデプロイする
  • プレーンなHTMLとJavaScriptで、スマートコントラクトOkojoArigatoを使う Web サイトをデプロイする

では、始めましょう。

Remix IDE で web3.js を使ってみる

まずは、Remix IDE で web3.js を使ってみましょう。Remix IDE で、JavaScript のファイルを作成します。

Remix IDE File Explorer
Remix IDE の File Explorer から、新規ファイルを追加

ファイル名は、とりあえず web3.js で遊ぶので、web3.playground.js とかにしてみました。

とりあえず、ファイルは、空にしますが、Promise を使う関数を処理したいので、async をつけた無名関数の中で遊ぶことにします。

// web3.playground.js

(async() => {
   // ここにコードを書いて遊ぶ
})();

では、web3 を呼び出してみましょう。Remix IDE では、web3 は、すでにインポートされているのでそのまま実行が可能です。web3.js のドキュメントは、こちらで確認できます。このドキュメントを読みながら試してみましょう。

接続しているアカウントを調べる

まずは、接続しているWeb3のアカウントを調べてみましょう。デフォルトでは、Remix VM というネットワークが使われます。これは、もっぱら開発に使われる永続化されないネットワークでガス代もかかりますが、100ETHが割り当てられているので全く気にすることなく開発ができます。Remix IDE の画面の説明は、前回の記事を参照してください。

Remix IDE の左側のタブから、Deploy & Run Transactions というタブを選択します。

Remix VM で web3.js を試す

この画面では、Account が、 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 となっています。このアカウントの ETH 残高を見てみましょう。

// web3.playground.js

(async() => {
  const accounts = await web3.eth.getAccounts();
	const account = accounts[0];
	console.log("Account", account);
	const balance = await web3.eth.getBalance(account);
	console.log("Wei", balance);
	console.log(await web3.utils.fromWei(balance, "ether"));
})();

例えば、このようなコードを書いてみましょう。Remix IDE からすぐに JS は、実行可能です。方法は、簡単で、いかにも実行しそうなプレイボタンをクリックすると実行されます。

Remix IDE では、プレイボタンからJSコードも実行可能

上記のスクショで写っているものは、先の内容も含んでおりますが、気にせずコードを実行してみましょう。すると、次の値がデバッグペインに表示されると思います。なぜ表示されるのかというと、 console.log(…)という関数が中身を表示する関数だからです。

Account
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
Wei
99999999999998477690
99.99999999999847769

正確には、Wei 以下のところはデプロイでGAS代がかかっているので、100ETHより小さくなリマス。

コードを一行ずつ読んでみましょう。

const accounts = await web3.eth.getAccounts();
const account = accounts[0];

このコードは、web3 から、eth のネットワークを指定して、ethereum ネットワークで使えるアカウントを全て取得しています。Remix IDE では、web3 へ接続する Provider が先ほど説明した、Deploy & Run Transaction というタブで設定されるので、そのタブの中で設定した Remix VM のアカウントが全て表示されます。

getAccounts() は、利用可能な全てのアカウントを取得するので配列になっています。どうすれば「利用可能」となるのかは、後で説明するので、Remix VM で利用可能になっているものと理解しておいてください。accounts 全体を見てみます。

// web3.playground.js

(async() => {
  const accounts = await web3.eth.getAccounts();
	console.log("Accounts", accounts);
})();

すると次のような出力が得られました。

Account
[
 "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
 "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
 "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db",
 "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
 "0x617F2E2fD72FD9D5503197092aC168c91465E7f2",
 "0x17F6AD8Ef982297579C203069C1DbfFE4348c372",
 "0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678",
 "0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7",
 "0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C",
 "0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC",
 "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c",
 "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
 "0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB",
 "0x583031D1113aD414F02576BD6afaBfb302140225",
 "0xdD870fA1b7C4700F2BD7f44238821C26f7392148"
]

一方で、Deploy & Run Transaction タブで、Account の選択に出てくるものを確認してみます。

Deploy & Run Transaction タブでアカウントを表示

中略されていますが、利用可能なアカウント一覧を console.log した結果と順番も含めて一致していました。

const balance = await web3.eth.getBalance(account);
console.log("Wei", balance);
console.log(await web3.utils.fromWei(balance, "ether"));

次に、 getBalance のコードを確認します。まず、非同期で web3.eth.getBalance(account) で一番上のアカウントの残高を表示します。次に、それをそのまま表示させています。この状態ですと、 99999999999998477690 となって、桁が大きすぎた一体いくらなのかわからないですね。この最小単位を wei で表します。ETH単位に変換するときに使える便利メソッドが、 web3.utils.fromWei() です。ここでは、ether単位に変換させていきます。そうしますと 99.99999999999847769 と変換されるので、大体 100 ETH あるなっていうのが分かりやすくなりました。

ERC20 スマートコントラクトを Remix VM にデプロイして web3.js から呼び出す

次に、前回の記事の OkojoCoin.sol を使ってみましょう。OkojoCoin.sol は、次のスマートコントラクトでした。名前が、Okojo Coin で、シンボルが、OKJC です。デプロイ時(コンストラクタが実行される時)に一兆 OKJC がミントされます

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract OkojoCoin is ERC20, Ownable {
    constructor() ERC20("Okojo Coin", "OKJC") {
        _mint(msg.sender, 1000000000000 * 10 ** decimals());
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

これをRemix VMにデプロイしておきましょう。

ABI とコントラクトアドレスから、特定のコントラクトを呼び出す

コンパイルした時に、ABI (Application Binary Interface)という小さなアイコンが現れるので、このボタンをクリックして ABI をコピーしましょう。

ABIをコピーする

コピーした、ABI をペーストします。長いですが、ここにも貼っておきます。これが、スマートコントラクトのインターフェース(使い方)を定義しています。

[
  {
    "inputs": [],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "subtractedValue",
        "type": "uint256"
      }
    ],
    "name": "decreaseAllowance",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "addedValue",
        "type": "uint256"
      }
    ],
    "name": "increaseAllowance",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "mint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

JSONデータの配列になっていますね。一つ一つが、コントラクトから呼び出せるメソッドのインターフェースを説明しています。流石に全部を説明していると長くなりすぎますので、一つを取り出してみましょう。とりあえず、ERC20トークンの残高を確認する balanceOf メソッドを見てみます。

{
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }

balanceOf というのが、name 属性にあたらえられています。これが、関数(メソッド)名を表しています。inputs という配列は、受け取る値の型と名前を表し、上記の例だと、account という名前の引数で、方は、アカウントアドレスになっています。output は、メソッドを呼び出した後に出力される値の型を示しており、上記の例ですと、unit256 ですので、数値で返却されます。

stateMutability というのは、payable, non-payable, view などがあります。view というのは、その名の通り閲覧するだけの関数であり、トランザクションを発生させないためガス代がかかりません。payable, non-payable は、ETH のやり取りができるかできないかという違いがありますが、トランザクションを書き込むのでガス代が発生します。balanceOf は、 view なのでガス代が発生しません。

このように、インプットとアウトプット、そしてその関数が大体何をするのかがわかるようになっています。ABI を通して、web3.js からスマートコントラクトの機能を呼び出すことができるようになります。逆に言えば、ABIを通してコントラクトを理解するので ABI は、省略形でも十分使うことができるはずです。ERC20は、共通部分については、ABI が決まっているので、トークンのアドレスさえ指定すれば Metamask などすでに実装ずみのアプリケーションからも使えるようになるわけです。その意味で、規格に対応しているアプリケーションがあれば、コントラクトを使うための独自アプリケーションの開発は不要です。

では、OkojoCoin スマートコントラクトを呼び出してみます。どのコントラクトを呼び出せば良いかを指定するわけですが、スマートコントラクトをデプロイするとコントラクトアドレスが作成されるので、コントラクトアドレスを使って呼び出し先のスマートコントラクトを一意に特定します。

コントラクトアドレスは、デプロイタブから取得できます。

コントラクトアドレスを取得

このコピーアイコンをクリックするとコントラクトアドレスが得られます。 0xd9145CCE52D386f254917e481eB44e9943F39138 筆者の環境ではこちらになりました。今は、Remix VM 上でデプロイしているので、etherscan などには表示されませんので注意してください。

次のコードを実行します。abiArray のところは、長いので省略していますが、ABI 取得でコピーしてきたものをそのまま貼り付ければOKです。

// web3.playground.js <- Solidity ではなく、JavaScript のファイルです!

(async() => {
  // 利用可能なアカウントの取得
  const accounts = await web3.eth.getAccounts();
  // 一番上のアカウントを取り出す
  const account = accounts[0];
  // abiArray を代入
  const abiArray = [...]; // 省略しています。

  // 先ほどデプロイしたコントラクトのアドレス(適宜変更してください)
  const contractAddress = "0xd9145CCE52D386f254917e481eB44e9943F39138";
  // コントラクトインスタンスを生成
  const okojoContract = new web3.eth.Contract(abiArray, contractAddress);
  // balanceOf メソッドを呼び出し(非同期なので、await します)
  const okojoBalance = await okojoContract.methods.balanceOf(account).call();

  // 結果をコンソールに表示
  console.log("OKJC", okojoBalance);
  console.log(await web3.eth.utils.fromWei(okojoBalance, "ether"));
})();

コントラクトアドレスの部分は、皆さんがデプロイしたコントラクトを参照してください。実行してみます。すると次のようなアウトプットが表示されます。

OKJC
1000000000000000000000000000000
1000000000000

これで、既存のERC20スマートコントラクトを呼び出して利用するイメージが湧きました。

new web3.eth.Contract(abiArray, contractAddress) の部分で、コントラクトインスタンスを新規に作成しています。続いて、 await okojoContract.methods.balanceOf(account).call() で非同期にメソッドを呼び出しています。methods の後に、balankceOf をドット . で接続して引数も指定します。その後に、call() しなければ何も起こらないので注意してください。

OkojoCoin を送金してみる

では、stateMutability が view な balanceOf だけでなく送金に使う transfer もみてみましょう。transfer のインターフェースは、次のようになっています。

{
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  }

送金元のアドレスは、不要で、to と amount だけが指定されています。送金元は、現在ログインしているアカウントです。アウトプットは、bool になっているので成功したか失敗したかだけがわかる仕組みですね。

transfer に似ているもので、transferFrom というものもありますが、こちらは、コインを徴収する際に使われるもので、送金者の承認(approve)が必要なメソッドになっています。

さて、transfer してみましょう。Remix VM のダミーアカウントのうち、 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 へ送金してみます。送金元は、デプロイに使われたアカウントである 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 にします。10 OKJCを送金します。

この実行によりトランザクションが発生するので、ガス代が必ずかかります。また、先ほどは、call() で指定していましたが、send() で指定することになりますので、この辺も変わります。

便利なメソッドを使う

さて、送金の前に便利なメソッドを確認しておきます。先ほど、 web3.eth.utils.fromWei(okojoBalance, "ether") を使いました。逆に、例えば、1 OKJC を送信するために 0 を18個繋げるとどうなるでしょうか。手入力すれば間違いが起きやすいですし、web3js でエラーになります。このエラーの詳細については、こちらで確認いただくとして、どうすれば良いかだけ説明します。

web3.utils.toBN() は、BigNumber に変換します。.toString(16) は、ここから、16進数(hex)の文字列を作ります。

実際に実行してみましょう。

// Debug Console
> web3.utils.toBN('100').toString(16)
64

100の16進数表記である64 = 16^1 * 6 + 16^0 * 4 が表示されました。

次に、大きな数字を作るのも大変ですね。例えば、値は、wei で入力しますが、 0を18個もつけなければならず、10000000000000000000 と入力する必要があります。これで、10 OKJC になります。この場合は、toWei というメソッドを使います。

// Debug Console
> web3.utils.toWei('10', 'ether')
10000000000000000000

これで準備ができました。

早速トランザクションさせてみましょう。

トランザクションを記録するメソッドは、sendを使います。send の詳細は、こちらを参照ください。第一引数の options のなかで、from (送信元アドレス)だけが必須項目になっているので、これらを踏まえて実装します。

// web3.playground.js

(async() => {
  // 利用可能なアカウント一覧を取得
  const accounts = await web3.eth.getAccounts();
  // 一番初めのアカウントを取得(これがデプロイに使われたアカウントです)
  const account = accounts[0];
  // abiArray を設定
  const abiArray = [...]; // 省略しています。

  // コントラクトのアドレス
  const contractAddress = "0xd9145CCE52D386f254917e481eB44e9943F39138";
  // コントラクトをインスタンス化
  const okojoContract = new web3.eth.Contract(abiArray, contractAddress);

  // 2番目のアカウント、こちらに送金します
  const transferTo = accounts[1]; 
  // 送金前の残高を確認、0が期待されます
  console.log('OKJC' await okojoContract.balanceOf(transferTo));

  // transfer メソッドを送信して、トランザクションを書き込みます
  await okojoContract.methods.transfer(
    // 第一引数の送信先アドレス
    transferTo,
    // 第二引数の送金金額(16進数であることを明示するため 0x を先頭につけます)
    '0x' + web3.eth.toBN(web3.utils.toWei('10.0', 'ether')).toString(16)
  ).send({
    // トランザクションの送り主、典型的には自分自身のアカウントです
    from: account
  });
  // 送金後の残高を確認、10 OKJC 増えているのが期待されます
  console.log('OKJC', await okojoContract.methods.balanceOf(transferTo).call());
})();

これを実行すると、次のようにデバッグコンソールに表示されます。

OKJC
0 #<- 送金前の残高
{"transactionHash":"0xb2e485dd9f41f00416d72aa39b038ceec376f3b8a261b540fae3e07d9e558af7","transactionIndex":0,"blockHash":"0x2441de29930365c693da348d61ffa116e7bb4863102e56c5f03d2af6239b90f8","blockNumber":8,"gasUsed":52161,"cumulativeGasUsed":52161,"status":true,"to":"0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99","events":{"Transfer":{"logIndex":1,"blockNumber":8,"blockHash":"0x2441de29930365c693da348d61ffa116e7bb4863102e56c5f03d2af6239b90f8","transactionHash":"0xb2e485dd9f41f00416d72aa39b038ceec376f3b8a261b540fae3e07d9e558af7","transactionIndex":0,"address":"0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99","id":"log_0eb19224","returnValues":{"0":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","1":"0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","2":"10000000000000000000","from":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","to":"0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","value":"10000000000000000000"},"event":"Transfer","signature":"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","raw":{"data":"0x0000000000000000000000000000000000000000000000008ac7230489e80000","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4","0x000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2"]}}}} 
OKJC
10000000000000000000 #<- 送金後の残高

一発でうまくいきました!!それは、筆者が、先に試しているからです。。。

送金前は、もちろん 0 です。そして、送金するときに、デバッグ表示として、トランザクションハッシュなどが表示され、きちんとブロックチェーンに書き込まれたことが確認できます。

送金後の残高は、 10000000000000000000 となっていますが、0を18個削除すると、 10 となりますので、10 OKJC をきちんと送金できていることがわかります。

まとめ

web3.js を使って、view系のメソッドを呼び出して残高を確認したり、non-payable メソッドを呼び出して送金たりしました。view 系では、call メソッドを使って呼び出し、non-payable 系では、send メソッドを使ってトランザクションを送信しました。また、send メソッドでは、送信元のアカウントが必須項目になっています。

これらのメソッド呼び出しで重要になる概念が、ABI です。ABI を使って web3.js は、スマートコントラクトにどんなメソッドが実装されているのかを理解しました。また、スマートコントラクトを一意に指定するには、コントラクトアドレスを使いました。

次に、より複雑なスマートコントラクトを実装してみましょう。

ERC20 トークンと感謝の言葉を送信する OkojoArigato スマートコントラクトを Remix VM にデプロイする

前回のブログで作成したERC20トークン(Okojo Coin)とメッセージを送るスマートコントラクト OkojoArigato を実装してデプロイしてみましょう。

まず、どんな機能が欲しいか考えてみましょう。

  • ありがとうの言葉と、OkojoCoin(OkojoArigato)を送信する (sendArigato non-payable)
  • 特定のアカウントが送った OkojoArigato 一覧を取得する (sentArigatoes view)
  • 特定のアカウントがもらった OkojoArigato 一覧を取得する (receivedArigatoes view)

とりあえずこのくらいでしょうか。スマートコントラクトは、後から変更するのが難しいので実際には頭を掻きむしっていろんなケースを考えつつできるだけシンプルな方法を考えましょう。

わかりやすいありがとう(OkojoArigato)の形

OkojoArigato を送信する際に必要な情報は、次のようなイメージです。

  • id : Arigato を一意に特定する数値
  • sender : Arigato を送信する(した)アカウントアドレス
  • receiver : Arigato を受信する(した)アカウントアドレス
  • comment : Arigato に添えるメッセージ
  • amount : 送信する OkojoCoin の量

エンジニアであれば、これがモデル定義、もしくは、テーブル定義と同等であると気がつくと思います。ブロックチェーンでは、データベースは、ブロックチェーンネットワークが代替します。

これを、struct (構造体)と呼ばれるもので、データの形を定義して行きます。Cでは、そのまま struct、TypeScript では、Interface とか type と呼ばれたりしますし、Ruby では、型指定がないものの厳密に管理された Hash がその役割に近いでしょう(ActiveRecord インスタンスだと機能が多すぎて比較になりません)。

例えば、前述した情報を struct で表現してみます。

struct Arigato {
  uint id;
  address sender;
  address receiver;
  string comment;
  uint256 amount;
}

情報に型を指定しました。

これをテーブルのように後から一覧を参照できるようにするには、単純に配列を宣言すれば良いです。

Arigato[] public arigatoes;

Arigato[] という記述は、Arigato の構造体で定義されたデータのリスト [] を宣言しています。public というのは、公開されているので誰でもアクセス可能になり、 arigatoes は、単なる名前です。日本語のありがとうに英語の複数形の s をつけているので違和感がありますが、あくまで名前なので、ちょうど良い違和感に感じます。

これらの設計から、スマートコントラクトをスケッチしてみましょう。メソッドの中身は、からのままです。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract OkojoArigato {

    // Arigato のデータ型、データベースのカラム設計と同じ
    struct Arigato {
        uint id;
        address sender;
        address receiver;
        string comment;
        uint256 amount;
    }

    // Arigato のテーブル
    Arigato[] public arigatoes;

    // okojoCoin
    IERC20 public okojoCoin;

    // okojoAddress で初期化して、ERC20トークンを結びつける
    constructor(address okojoAddress)  {
        okojoCoin = IERC20(okojoAddress);
    }

    // sendArigato non-payable
    function sendArigato(
        string memory comment, 
        address to, 
        uint256 amount) public returns (bool)
    {
        // TODO
    }

    // sentArigatoes view
    function sentArigatoes(address sender) public view returns (Arigato[] memory) {
        // TODO
    }

    // receivedArigatoes view
    function receivedArigatoes(address receiver) public view returns (Arigato[] memory) {
        // TODO
    }
}

なんかこんな感じかなぁ?くらいの感じでスケッチして行きます。他のプログラミングでもそうですが、コツは、詳細よりも枠組みをしっかり作っていくことでしょうか。デッサンのコツに似ています。デッサンは、下におすすめのチャンネルを貼っておきます。筆者は、プログラミングに限らず、なんでも大枠をざざっと捉えてから細部を仕上げるようにしています。

[Eng sub] How to draw Paper box with a pencil / Easy drawing for beginners / Step by Step

Remix VMに未完成のスマートコントラクトをデプロイしてみる。

Remix VM でとりあえずデプロイしてみましょうか。Remix VM は、永続化されませんし、無料ですし試すにはピッタリです。デプロイしてみると、OkojoAddress はなにか?と聞かれます。これは、コンストラクタの引数に OkojoAddress を指定しているからですね。

OkojoAddress は、OkojoCoin のコントラクトアドレスを指定すれば良いのです。

コンストラクタの引数は、デプロイの時に必要

現状、OkojoCoin は、すでにデプロイされていますので、コントラクトアドレスをコピーします。 下の図のコピーアイコンからコピーすればコントラクトアドレスが取得できます。0xd9145CCE52D386f254917e481eB44e9943F39138 が取得できました。

コントラクトアドレスを取得

これを先ほどの OkojoAddress のところに貼り付けてデプロイします。今デプロイしちゃっても大丈夫ですが、中身が空なので、ちょっと実装してからにしましょう。

sendArigato (ありがとうを送る)を実装する

さて、ありがとうを送信する機能を実装していきましょう。ありがとうのインターフェースは、先ほど掲載しましたが、再掲します。

    // sendArigato non-payable
    function sendArigato(
        string memory comment, 
        address to, 
        uint256 amount) public returns (bool)
    {
        // TODO
    }

TODO の中身を書いていきます。まずは、引数について確認しましょう。

string memory comment は、文字列でコメントを引数にとることを表しています。memory というのは保存領域です。コンピューティングリソースを使うので、保存方法によってガス代が異なりますが、ここでは、memory に保存するのかと理解しておいてください。

address to アカウントアドレス to にありがとうを送ります。 to だとわかりにくい可能性もあるので、もう少し長い名前にしても良いかもしれません。

uint256 amountは、送信するERC20トークンの量になります。wei が単位です。

public というのは、このクラス外からも呼び出せる関数で、 returns (bool) は、true か false を返却します。想定としては、返却されるのは true だけどそれ以外は例外が投げられることになります。

まず、この取引が成立する必要条件を考えてみます。

ありがとうを成立させる条件

  • 何かしらのコメントを添えること
  • to アドレスが有効なアドレス(nullではない)であること
  • 送信するトークンの量 amount は、 0 以上で何かしらを送信すること
  • 自分に送るありがとうではないこと

まずは、このくらいでしょうか。あとは、中で transferFrom(sender, receiver, amount) を呼び出すので、ERC20トークンの方で、そのやり取りが承認されている必要があります。ERC20の承認 approve は、どのようになっているでしょうか。GitHub でソースコードを調べます。

openzeppelin-contracts/ERC20.sol at master · OpenZeppelin/openzeppelin-contracts

approve に関連する記述は、こちらの行にあります。抜粋します。

function approve(address spender, uint256 amount) public virtual override returns (bool) {
   address owner = _msgSender();
   _approve(owner, spender, amount);
   return true;
}

これをみると、private 関数の _approve に入る owner は、 _msgSender() となっています。書いてあることをそのまま日本語を使いながら繋げただけの説明になってしまっていますが、要するに、このERC20コントラクトに approve を送信した人が自動的に承認者になります。例えば、あるアカウントからERC20 コントラクトに、直接 approve を送信すれば送信者アカウントが msg.sender になります。一方、あるアカウントが、別のスマートコントラクトを介して ERC20 コントラクトに approve を送信しようとすると、スマートコントラクトのアドレスが msg.sender になります。このことから、ERC20コントラクトで、トークンのやり取りを委譲するには、元のスマートコントラクトと直接トランザクションさせる必要があるということがわかります。

次に、transferFrom もみてみましょう。

function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
    address spender = _msgSender();
    _spendAllowance(from, spender, amount);
    _transfer(from, to, amount);
    return true;
}

この中身を見ると、 fromto とは別に、中で spender が定義されています。 spender の方は、 _msgSender() となっておりますので、典型的にはスマートコントラクトのアドレスになると思われます。 from のアカウントからスマートコントラクトが委譲された量の範囲内であれば to (これはどこでも良い) 宛に ERC20 トークンを送金できる仕組みになっています。

整理しますと、次のようになっています。

from から、to へありがとうを送信する単純化したシーケンス

まず、from から OkojoCoin に approve を送信します。ここでは、100 OKJC を OkojoArigato に承認します。100e18 というのは100の後ろに0が18個つくことを省略した書き方です。次に、from は、 sendArigatoを OkojoArigato に送信します。このリクエストを受けて、OkojoArigato は、transferFrom を使って、OkojoCoin の送金を完了させます。to は、いるだけで絡んできません。

され、sendArigato を成立させる条件の他に、transferFrom を成立させる条件もチェックするようにしましょう。

transferFrom を成立させる条件

  • from の OkojoCoin の残高が送金額よりも大きいこと
  • from が OkojoArigato に承認しているトークンの量が、送金額よりも大きいこと

くらいでしょうか。

ありがとうを成立させる条件(まとめ)

  1. 何かしらのコメントを添えること (実は、Solidity では、空の文字列を特定するのが少し複雑ですのでこの条件はとりあえず外します)
  2. to アドレスが有効なアドレス(nullではない)であること
  3. 送信するトークンの量 amount は、 0 以上で何かしらを送信すること
  4. from の OkojoCoin の残高が送金額よりも大きいこと
  5. from が OkojoArigato に承認しているトークンの量が、送金額よりも大きいこと
  6. 自分に送るありがとうではないこと(from=to ではないこと)

こんなに調べる必要あるのかって感じもありますが、一応やっておきます。まず、from のERC20残高とOkojoArigato に承認している金額を取得しましょう。

uint256 balance = okojoCoin.balanceOf(msg.sender);
uint256 allowance = okojoCoin.allowance(msg.sender, address(this));

balanceOf は、すでに何度も出てきています。allowance の方は、こちらで定義されています。ドキュメントも便利ですが、短いコードであれば直接読むのが一番正確に理解できます。第一引数が、コインのオーナーで、第二引数が、委譲先のアドレスです。今回の場合、Arigato を送信する人が、オーナーのコインだけ確認すれば良いので、 msg.sender を指定します。第二引数は、コントラクト自体を指定しなければならないのですが、デプロイ前はアドレスが確定しません。なので、 address(this) を指定しておけば大丈夫です。

では、必須条件の確認をしましょう。必須条件の確認は、 require を使います。 require の第一引数に条件、第二引数にエラーメッセージをつけます。エラーメッセージを英語にしていますが、短いので英語で書きましょう。

// 1. 何かしらのコメントを添えること→省略
// 2. to アドレスが有効なアドレス(nullではない)であること
require(to != address(0), "Okojo Arigato: sendArigato to zero address");
// 3. 送信するトークンの量 amount は、 0 以上で何かしらを送信すること
require(amount > 0, "Okojo Arigato: Amount must be greater than 0");
// 4. from の OkojoCoin の残高が送金額よりも大きいこと
require(balance >= amount, "Okojo Arigato: Transfer amount exceeds OKJC balance");
// 5. from が OkojoArigato に承認しているトークンの量が、送金額よりも大きいこと
require(allowance >= amount, "Okojo Arigato: Insufficient OKJC allowance");
// 6. from と to が実は同じではないこと
require(msg.sender != to, "Okojo Arigato: Addresses are identical");

こちらでやっておきましょう。

実際に送信するロジックを実装します。ERC20 の transferFrom をそのまま使ってしまいましょう。

// 送金実行
okojoCoin.transferFrom(msg.sender, to, amount);

では、Arigato を作成し、一覧に追加します。(これ実はあとで消しますが、とりあえずお付き合いください)

// ありがとうを登録
Arigato memory arigato = Arigato(
    arigatoes.length,
    msg.sender,
    to,
    comment,
    amount
);
// 一覧に追加
arigatoes.push(arigato);

簡単ですね。 Arigato 生成時に、id に arigatoes.length を使っていますが、これで0から連番で自動発番されるようになっています。 arigatoes.push も配列に追加しているに過ぎません。これでありがとうが送信され追加されるようになったでしょうか。全体の function をみてみましょう。

// sendArigato non-payable
function sendArigato(
    string memory comment, 
    address to, 
    uint256 amount) public returns (bool)
{
    // ERC20 オコジョコインの残高や、送信先への送信可否を判定するためのロジック
    // 送信者が承認している送金額
    uint256 allowance = okojoCoin.allowance(msg.sender, address(this));
    // 送信者が持っている残高
    uint256 balance = okojoCoin.balanceOf(msg.sender);

    // 必須条件の確認
    // 1. 何かしらのコメントを添えること→省略
    // 2. to アドレスが有効なアドレス(nullではない)であること
    require(to != address(0), "Okojo Arigato: sendArigato to zero address");
    // 3. 送信するトークンの量 amount は、 0 以上で何かしらを送信すること
    require(amount > 0, "Okojo Arigato: Amount must be greater than 0");
    // 4. from の OkojoCoin の残高が送金額よりも大きいこと
    require(balance >= amount, "Okojo Arigato: Transfer amount exceeds OKJC balance");
    // 5. from が OkojoArigato に承認しているトークンの量が、送金額よりも大きいこと
    require(allowance >= amount, "Okojo Arigato: Insufficient OKJC allowance");
        
    // 送金実行
    okojoCoin.transferFrom(msg.sender, to, amount);

    // ありがとうを登録
    Arigato memory arigato = Arigato(
        arigatoes.length,
        msg.sender,
        to,
        comment,
        amount
    );

    arigatoes.push(arigato);

    return true;
}

うん。なんかよくできていそうな雰囲気ですが、 Arigato[] ってどのくらい大きくなっていいの?とか、 transferFromだけ成功して、 arigatoes.push が失敗したらどうなるの?とか、ちょっと考えてみましょう。

function の一部だけ成功することはない

この点に関しては、心配無用です。Solidity の function は、その設計からアトミック(原子的)になっています。どういうことかというと、function の一部だけが成功するということはなく、どこかで失敗すれば、全てが失敗し、function が実行されていない状態と同じになります。成功するには、function のなかの全てが成功しなければなりません。

失敗させる要素は、 require で失敗するか呼び出した function ないでの検証にエラーになるか複数の可能性がありますが、function 全体が成功するか、全てが実行されないかのどちらかの状態しかありません。

しかしながら、失敗するまでに実行された function のガス代はかかってきますので、失敗するのであればなるべく早く失敗してほしいです。そういうわけで、require でチェックを沢山しておきました。

コントラクトのスケーラビリティについて考える

Ethereum ブロックチェーンでは、取引情報が格納され、スマートコントラクトも同様に保存されています。スマートコントラクト内に保存されている Arigato[] は、すべてのノードで共有され容量が大きくなるとそれだけコスト、つまり、ガス代がかかるようになります。スケーラブルなスマートコントラクトにするには、この仕組みはあんまりよろしくない気もします。

先ほどスケッチしたときにある他の機能は、自分の送受信したありがとうだけ取得できれば良いものです。それならば、取引のイベントログから取得することも一考に値します。また、イベントログを使うことで、送受信が発生したときにフロントエンドのアプリケーションで通知が受け取れる利点もあります。

実装を変えて、イベントを利用するようにしましょう。一覧取得はフロントエンドで実行します。もちろんこの方法も完璧ではありません。ありがとうが増えればパフォーマンスの低下は免れないでしょう。その場合は、イベントログをオフチェーンのデータベース(旧来型データベース)に格納して高速化を図るなどができます。では、イベントログでありがとうを取得するように変更してみましょう。バックエンド処理をするスマートコントラクトはもっとシンプルになります。

イベントを使って取引を記録する

イベントを定義するのは、簡単です。 event で宣言すれば良いです。先ほど定義したわかりやすいありがとうの形は、もう使いません。代わりに Arigato イベントを定義してみます。

event Arigato(
    address indexed sender, 
    address indexed receiver,
    uint256 amount,
    string comment
);

id は、なくなりましたが、わかりやすいありがとうの形をほぼ踏襲したイベントができました。新たに indexed という修飾子が登場しました。これは、簡単にいうと検索しやすくするための記号で、 senderreceiverを使って検索する可能性が高い場合につけておきます。自分の送受信したありがとうは検索したいので、送信、受信共に indexed をつけています。

では、 push していたところは、どうなるかというと次のようになります。

emit Arigato(sender, receiver, amount, comment);

完了です。とてもシンプルになりました。最後にイベントログ形式で保存するスマートコントラクト全体を記載しておきます。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract OkojoArigato {

    IERC20 public okojoCoin;

    // Arigato イベントを宣言
    event Arigato(address indexed sender, address indexed receiver, uint256 amount, string comment);

    // okojoAddress で初期化して、ERC20トークンを結びつける
    constructor(address okojoAddress)  {
        okojoCoin = IERC20(okojoAddress);
    }

    // sendArigato non-payable
    function sendArigato(
        string memory comment, 
        address to, 
        uint256 amount) public returns (bool)
    {
        // ERC20 オコジョコインの残高や、送信先への送信可否を判定するためのロジック
        // 送信者が承認している送金額
        uint256 allowance = okojoCoin.allowance(msg.sender, address(this));
        // 送信者が持っている残高
        uint256 balance = okojoCoin.balanceOf(msg.sender);

        // 必須条件の確認
        // 1. 何かしらのコメントを添えること→省略
        // 2. to アドレスが有効なアドレス(nullではない)であること
        require(to != address(0), "Okojo Arigato: sendArigato to zero address");
        // 3. 送信するトークンの量 amount は、 0 以上で何かしらを送信すること
        require(amount > 0, "Okojo Arigato: Amount must be greater than 0");
        // 4. from の OkojoCoin の残高が送金額よりも大きいこと
        require(balance >= amount, "Okojo Arigato: Transfer amount exceeds OKJC balance");
        // 5. from が OkojoArigato に承認しているトークンの量が、送金額よりも大きいこと
        require(allowance >= amount, "Okojo Arigato: Insufficient OKJC allowance");
        
        // 送金実行
        okojoCoin.transferFrom(msg.sender, to, amount);

        // ありがとねイベント発火してレコードを記録
        emit Arigato(msg.sender, to, amount, comment);

        return true;
    }
}

コメントがもりもりに書いてありますが、transferFrom を呼び出してイベントを発火するだけのシンプルなものになりました。

OkojoArigato をデプロイしてテストする

複数のアドレスを使ってテストするので、テストの手順を整理しておきましょう。

成功ケース

以下全て、Account 1 (0x5B38Da6a701c568545dCfcB03FcB875f56beddC4)で実行しましょう。

  1. (必要に応じて)OkojoCoin をデプロイして、OkojoCoinのコントラクトアドレスをコピー(①)
  2. OkojoArigato をコンパイルして①を入力してデプロイ、OkojoArigato のコントラクトアドレスをコピー(②)
  3. OkojoCoin の approve で、spender に②を入れて amount をたとえば、1000e18 くらいにして実行
  4. OkojoArigato の sendArigato で、to に Account 2(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2)、commentに「ありがとう助かりました。」、amout に 10e18 を入力して実行
  5. OkojoCoin の balanceOf で、Account 1 と Account 2 の残高を確認する

期待する結果は、次の通りです。

  • 全てエラーが発生することなく実行できること
  • Account 1 の残高は、999999999990e18 となっていること
  • Account 2 の残高は、10e18 となっていること

さてやってみましょう。うまくいったと思います。手順にミスがあるとうまくいかないので注意してください。

失敗ケース

  • Account 3 で、成功ケースの3以降を実行すると、OkojoCoin 残高が不足しているので「Okojo Arigato: Transfer amount exceeds OKJC balance」エラーが表示されます
  • Account 2 で、成功ケースの4以降を実行すると、approve されていないので、「Okojo Arigato: Insufficient OKJC allowance」エラーが表示されます
  • どのアカウントでも、amount=0 とすると「Okojo Arigato: Amount must be greater than 0」エラーが表示されます
  • どのアカウントでも、to=0 とすると「Okojo Arigato: sendArigato to zero address」エラーが表示されるかと思いきや、その手前で、invalid address エラーが表示されます。
  • どのアカウントでも、to=自分のアカウントとすると「Okojo Arigato: Addresses are identical」エラーが表示される

Web3.js で、Arigato 一覧を取得する

さて、いろんなアカウントで Arigato を発火させ合ってみてください。そうしないとデータがなくて寂しいので。しばらくやり取りしたら、新たなファイル OkojoArigato.web3.js を作成しましょう。

今回は、OkojoArigato のコントラクトを呼び出します。

筆者の環境では、コントラクトアドレスが、 0x417Bf7C9dc415FEEb693B6FE313d1186C692600F になっています。ABI も貼り付けます。

[
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "okojoAddress",
				"type": "address"
			}
		],
		"stateMutability": "nonpayable",
		"type": "constructor"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "address",
				"name": "sender",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "receiver",
				"type": "address"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			},
			{
				"indexed": false,
				"internalType": "string",
				"name": "comment",
				"type": "string"
			}
		],
		"name": "Arigato",
		"type": "event"
	},
	{
		"inputs": [
			{
				"internalType": "string",
				"name": "comment",
				"type": "string"
			},
			{
				"internalType": "address",
				"name": "to",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			}
		],
		"name": "sendArigato",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "okojoCoin",
		"outputs": [
			{
				"internalType": "contract IERC20",
				"name": "",
				"type": "address"
			}
		],
		"stateMutability": "view",
		"type": "function"
	}
]

ERC20 よりもシンプルですね。 constructor、event Arigato、okojoCoin、sendArigato だけが定義されています。

(async() => {
  const accounts = await web3.eth.getAccounts();
  const account = accounts[0];
  const abiArray = [...];
  const contractAddress = "0x417Bf7C9dc415FEEb693B6FE313d1186C692600F";
  const okojoArigato = new web3.eth.Contract(abiArray, contractAddress);
})();

これで、まずは、okojoArigato の function にアクセスできるようになりました。

ありがとう一覧をフロントエンドで解析するという雑な判断をしたので、ちょうど良い閲覧系の function がありません。そこで、okojoCoin のアドレスを取得して表示してみましょう。

okojoCoin は、public で宣言されているメンバ変数になりますので、自動的に getter が作成されています。web3.js から呼び出すには、getter 関数経由で呼び出すことになりますので、次のようになります。

(async() => {
  const accounts = await web3.eth.getAccounts();
  const account = accounts[0];
  const abiArray = [...];
  const contractAddress = "0x417Bf7C9dc415FEEb693B6FE313d1186C692600F";
  const okojoArigato = new web3.eth.Contract(abiArray, contractAddress);
  console.log(await okojoArigato.methods.okojoCoin().call());
})();

// 0xd9145CCE52D386f254917e481eB44e9943F39138

少なくとも okojoArigato にアクセスできることを確認しました。

web3.js から、過去の Event を呼び出す

過去のイベントを呼び出してみましょう。event のAPI仕様については、こちらから確認できます。これを見ると、 okojoArigato.events.Arigato({...}) とすると、Event を Subscribe(購読)することができるようです。こうしておけば、取引が発生した際に、画面を即座に変更するなどができて便利そうです。

さて、過去のイベントは、どうかというと、getPastEvents というピッタリの名前のAPIがあるのでこちらを見てみましょう。第一引数にイベント名、上記の例ですと Arigato を文字列で入力し、第二引数にオプション設定をいろいろとするようです。オプションはすっ飛ばして早速実行してみましょう。

(async() => {
  const accounts = await web3.eth.getAccounts();
  const account = accounts[0];
  const abiArray = [...];
  const contractAddress = "0x417Bf7C9dc415FEEb693B6FE313d1186C692600F";
  const okojoArigato = new web3.eth.Contract(abiArray, contractAddress);
  const events = await okojoArigato.getPastEvents('Arigato');
  console.log(events);
})();

// []

何も表示されないです。オプションをすっ飛ばしたからかもしれないので、オプションの中身を見てみます。すると、filter、fromBlock、toBlock、topics という項目があります。filter は、表示を減らす方なので、fromBlock と toBlock が関係してそうです。

例に従って、fromBlock を0、toBlock を ‘latest’ にしてみましょう。

(async() => {
  const accounts = await web3.eth.getAccounts();
  const account = accounts[0];
  const abiArray = [...];
  const contractAddress = "0x417Bf7C9dc415FEEb693B6FE313d1186C692600F";
  const okojoArigato = new web3.eth.Contract(abiArray, contractAddress);
  const events = await okojoArigato.getPastEvents('Arigato', {
    fromBlock: 0, toBlock: 'latest'
  });
  console.log(events);
})();

// [{"logIndex":1,"blockNumber":10,"blockHash":"0x6239200b607a462d89a83ce50a379baaf60fb28898430b95fcb45dee33439466","transactionHash":"0x3f46a7d03253811f7aa250cf491313836f451746c13157ef471d48e595984b45","transactionIndex":0,"address":"0x417Bf7C9dc415FEEb693B6FE313d1186C692600F","id":"log_89abaf20","returnValues":{"0":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","1":"0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db","2":"10000000000000000000","3":"あんがとさん","sender":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","receiver":"0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db","amount":"10000000000000000000","comment":"あんがとさん"},"event":"Arigato","signature":"0xdb0e3f1fa8151a453ed623a09d6ecba9dff1680082df4c1659601b5053f5f4f7","raw":{"data":"0x0000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000012e38182e38293e3818ce381a8e38195e382930000000000000000000000000000","topics":["0xdb0e3f1fa8151a453ed623a09d6ecba9dff1680082df4c1659601b5053f5f4f7","0x0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4","0x0000000000000000000000004b20993bc481177ec7e8f571cecae8a9e22c02db"]}}]

何やら取得されていますね。ついでに新しいイベントが発生したときにどうなるか試してみましょう。自分が、receiver になった場合を試します。

okojoArigato.events.Arigato({
  filter: { reciever: account },
  fromBlock: 0
}, (error, event) => {
  console.error(error);
  console.log(event);
})
.on('connected', (subscriptionId) => {
  console.log(subscriptionId);
})
.on('data', (event) => {
  console.log(event);
});

こんな感じかな。しかし、関数の中で EventListener を設定しても動かなそうなので、そろそろ Remix IDE だけでやる限界を感じてきました。

この辺で Web 画面を開発しながらデバッグ環境を整えていきましょう。今回は、Hardhat を使ってみます。

Hardhat で Dapp 開発環境を建てる

Hardhatを使うと非常に簡単に開発環境を建てられそうです。公式ページのガイドに従って開発環境を立てていきます。Windows の方は、WSL2 で実行することが推奨されています。

コンソールを立ち上げましょう。開発用のフォルダまであらかじめ移動しておきます。例えば、

$ cd development
$ mkdir OkojoArigato
$ cd OkojoArigato

みたいにすれば、OkojoArigato というプロジェクトフォルダを作成して、プロジェクトフォルダに入れます。

前提条件として、NodeJSのインストールが必要ですが、それは、次のリンクからファイルをダウンロードしてインストrーうしましょう。

NodeJSインストール:https://nodejs.org/en/download

では、hardhat を実行します。npx というコマンドは、インストールしてあってもなくてもいきなり実行できる便利なコマンドなので積極的に使いましょう。npxがインストールされていないときは、必要に応じて次のコマンドを実行してください。

$ npm install -g npx

これで、でインストールできます。

さて、npx hardhat を実行します。今回は、JavaScript でプロジェクトを作成します。

OkojoArigato $ npx hardhat
# インストール中。。。。

888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.13.0 👷‍

? What do you want to do? … 
❯ Create a JavaScript project
  Create a TypeScript project
  Create an empty hardhat.config.js
  Quit

✔ Hardhat project root: · /Users/shiro/development/OkojoArigato
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Help us improve Hardhat with anonymous crash reports & basic usage data? (Y/n) · y

✔ Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n) · y

TypeScript を選びたいところですが、紙面の都合上、JavaScript の方が良いと思われるので、JavaScript のプロジェクトを選択します。TypeScript が好きな方は、問題なくタイプスクリプトでできると思うのでやってみてください。他は全部デフォルトで、Enter 連打します。

実行したフォルダ内にいくつかファイルとフォルダが作成されています。/contracts フォルダ内に、 OkojoCoin.solOkojoArigato.sol をコピーしておきましょう。このフォルダ内のスマートコントラクトがコンパイルされます。

今のままですと、openzeppelin のコードをインポートできないので、インストールします。

$ npm install @openzeppelin/contracts

インストールできたら、コンパイルします。次のコマンドで、 /contracts 以下のファイルをすべてコンパイルします。

$ npx hardhat compile

しばらくすると(ほぼ一瞬で)コンパイルが完了します。

ABI は、どこにあるのかというと、 /artifacts というフォルダの OkojoArigato.json というファイルが、JSON形式になっていて、 abi というプロパティを持っています。その値が ABI 本体になります。ABI は、それをコピーして使いましょう。あとは、デプロイしてコントラクトアドレスを取得すれば web3.js から使えるようになります。

Hardhatのローカルノードを建てて、ローカルにデプロイ

Hardhat の機能でローカル環境にテストネットを構築します。これも非常に簡単で、 npx hardhat node で完了です。10000 ETH を持った20個のアカウントとプライベートキーが発行されます。プライベートキーとアドレスがあるので、Metamask でインポート可能になります。

OkojoArigato % npx hardhat node
(node:24922) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
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

Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

Account #3: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

Account #4: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a

Account #5: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba

Account #6: 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

Account #7: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356

Account #8: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000 ETH)
Private Key: 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97

Account #9: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000 ETH)
Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Account #10: 0xBcd4042DE499D14e55001CcbB24a551F3b954096 (10000 ETH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897

Account #11: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 (10000 ETH)
Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82

Account #12: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a (10000 ETH)
Private Key: 0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1

Account #13: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec (10000 ETH)
Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd

Account #14: 0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097 (10000 ETH)
Private Key: 0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa

Account #15: 0xcd3B766CCDd6AE721141F452C550Ca635964ce71 (10000 ETH)
Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61

Account #16: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30 (10000 ETH)
Private Key: 0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0

Account #17: 0xbDA5747bFD65F08deb54cb465eB87D40e51B197E (10000 ETH)
Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd

Account #18: 0xdD2FD4581271e230360230F9337D5c0430Bf44C0 (10000 ETH)
Private Key: 0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0

Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

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.

アカウントが作成されました。

発行したアカウントを Metamask にインポート

まずは、ネットワークを追加します。

Metamask にネットワークを追加

ネットワークを追加ボタンで、ネットワークの設定をします。先ほどの npx hardhat node で、JSON-RPC が、127.0.0.1:8545 であることがわかったので、次のように設定してください。

Hardhat を追加

では、使えるアカウントをインポートします。20すべてをインポートしても良いのですが、大変なので初めの一つだけインポートしていきましょう。

一つ目は、

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

でした。「アカウントをインポート」で秘密鍵を入力してインポートします。

アカウントのアイコンからアカウントをインポートをクリック

アカウントをインポート画面が表示されるので、秘密鍵、今の場合は、 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 を入力します。試したところ、秘密鍵は、いつも同じ値が出てくるので hardhat のなかで決まった値なのかもしれないです。ローカルのアカウントなので秘密鍵を晒していますが、絶対に人に見せてはいけない値です。

秘密鍵をペーストする

そうすると、アカウントがインポートされます。10000ETHも持ってますね。なるほどこれなら開発には困らないですね。

10000ETHあれば、開発には困らない

ローカルネットに開発環境を立てると便利ですが、アカウント作りすぎて訳わからなくなったり、Metamask の動作がおかしく見えることがあります。そんな時は、Metamask から開発用のアカウントを削除したりして、綺麗にしましょう。

hardhat のネットワークにスマートコントラクトをデプロイ

では、OkojoCoin と OkojoArigato をデプロイしていきましょう。Lock というサンプルのスマートコントラクトをデプロイするスクリプト scripts/deploy.js を開くと次のようになっています。

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
// will compile your contracts, add the Hardhat Runtime Environment's members to the
// global scope, and execute the script.
const hre = require("hardhat");

async function main() {
  const currentTimestampInSeconds = Math.round(Date.now() / 1000);
  const unlockTime = currentTimestampInSeconds + 60;

  const lockedAmount = hre.ethers.utils.parseEther("0.001");

  const Lock = await hre.ethers.getContractFactory("Lock");
  const lock = await Lock.deploy(unlockTime, { value: lockedAmount });

  await lock.deployed();

  console.log(
    `Lock with ${ethers.utils.formatEther(
      lockedAmount
    )}ETH and unlock timestamp ${unlockTime} deployed to ${lock.address}`
  );
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

これは、Lock.sol というスマートコントラクトをデプロイするスクリプトですが、キーになりそうなのは、 getContractFactory("Lock") というところですね。これで、デプロイするコントラクトを決めています。また、 Lock.deploy(...) で、コンストラクタの引数を渡していることがわかります。なので、これを真似て OkojoCoin のデプロイスクリプトを書いてみます。 getContractFactory については、こちらを参照してください。

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
// will compile your contracts, add the Hardhat Runtime Environment's members to the
// global scope, and execute the script.
const hre = require("hardhat");

const contractName = "OkojoCoin";

async function main() {
  const Contract = await hre.ethers.getContractFactory(contractName);
  const contract = await Contract.deploy();
  const ethAmount = 0;

  await contract.deployed();

  console.log(
    `Contract with ${ethers.utils.formatEther(
      ethAmount
    )}ETH and deployed to ${contract.address}`
  );
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

getContractFactory() の引数を、OkojoCoin に変えて、不要なところを削除したスクリプトになります。これで試してみましょう。

$ npx hardhat run scripts/deployCoin.js --network localhost
(node:26482) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:26483) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Contract with 0.0ETH and deployed to 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

適当にやりましたが、上手くいった雰囲気です。コントラクトアドレスが、 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 のスマートコントラクトがデプロイできました。

次は、OkojoArigato をデプロイする時です。OkojoArigato は、OkojoCoin のアドレスが必要になるので先ほどのコントラクトアドレスを暗記しておきます(コピペでも良い)。

デプロイスクリプトは、ほぼ同じですが、OkojoCoin のアドレスを使うところが変わりますね。

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
// will compile your contracts, add the Hardhat Runtime Environment's members to the
// global scope, and execute the script.
const hre = require("hardhat");

const contractName = "OkojoArigato";
// ここ!
const coinContract = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";
async function main() {
  const Contract = await hre.ethers.getContractFactory(contractName);
  const contract = await Contract.deploy(coinContract);
  const ethAmount = 0;

  await contract.deployed();

  console.log(
    `Contract with ${ethers.utils.formatEther(
      ethAmount
    )}ETH and deployed to ${contract.address}`
  );
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

これでデプロイしてみましょう。

$ npx hardhat run scripts/deployArigato.js --network localhost
(node:26641) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:26642) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Contract with 0.0ETH and deployed to 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0

またまた上手くいきました。コントラクトアドレスは、 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 です。これでスマートコントラクトをデプロイするところは完了です。

ここに重要な点が二つあります。

1つ目に、 simple.min.cssweb3.min.js を読み込んでいます。これは、CDN と言ってインターネット上で配布されているプログラムを読み込むものです。simple.css は、見た目を何となく綺麗にするためのもので、web3.js は、Remix IDE で触ってたあの web3 です。

2つ目に、 </body> の直前に、 <script> を読み込んでいます。ここにJavaScript プログラムを読み込みます。

コントラクトアドレスや、アカウントアドレス、秘密鍵は、ローカル開発環境ごとに変わる可能性がありますので、自分の環境のログ出力などを見ながら適宜調整していってください。

画面と機能を定義する

この画面でしたいことは、次のとおりです。

  • Metamask とこのアプリを接続して、ウォレットでログインする機能
  • OkojoCoin の一定量を OkojoArigato コントラクトに委譲する機能(ERC20 approve)
  • Arigatoを送信する機能
  • やり取りされた Arigato を一覧する機能
  • 上記の表示をはじめに取得してくる機能

画面の構成は、次のようにしてみます。配置とかは特に考えず、上から並べて行きます。

画面イメージ

ラフの画面イメージを作成しました。

スマートコントラクトと接続するウェブアプリ Dapps を作成する

ここからは、solidity だけでなく、HTML や JavaScript の基本が必要になってきます。ハードになりますが、すでにやってきたことを違うフォーマットで繰り返すだけなのでお付き合いいただけると嬉しいです。

まずは、ウェブサイトを表示させましょう。そのために、プロジェクトフォルダの中に ./public/index.html というファイルを作成してください。中身は、次のような感じから始めましょう。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Okojo Arigato</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <!-- なんとなく綺麗なレイアウトにする -->
    <link rel="stylesheet" href="<https://cdn.simplecss.org/simple.min.css>">
    <!-- web3.js を読み込む -->
    <script src="<https://cdn.jsdelivr.net/npm/web3@1.9.0/dist/web3.min.js>"></script>
  </head>
  <body>

  <script type="text/javascript">
    // ここにスクリプトを直打ちする
  </script>
  </body>
</html>

必要な機能を script 内 に書いていきましょう。JavaScript です。どれもネットワークとの接続が必要なので、 async 関数にして await を使えるようにしておきます。

// 認証状態
async function getLoginStatus() {};

// ログインする
async function login() {};

// ---- Action 系 ----
// 委譲する
async function approveOkojo(amount) {};

// ありがとうを送る
async function sendArigato(to, amount, message) {};

// ---- 表示系 -----
// ありがとうを取得する
async function getArigatoes() {};

// ログイン状態を取得する
async function getAccounts() {};

// 委譲されたオコジョコインの総額を取得する
async function getAllowance() {};

// オコジョコインの残高を取得する
async function getOkojoBalance() {};

これで実装していきましょう。

コードのスケッチを ChatGPT-4 に喰わせる

すでに Remix IDE で使ってきたことを繰り返すだけですので、手間を省きたいです。ChatGPT4 にコードのスケッチとヒントとなる情報を食わせます。深く考えずに長めのプロンプトを投げて調整します。

ChatGPT に食わせたプロンプト

次の Dapps の実装を完成させてください。OKJCとは、ERC20トークンです。ERC20トークンとコメントを互いに送り合うアプリです。
スマートコントラクトは、ER20のOkojoCoin コントラクトの他に、ありがとうを送信する OkojoArigato スマートコントラクトがあります。
ユーザーは、OkojoArigato に権限を委譲してOkojoCoinを送金します。OkojoArigato の中には、
```
function sendArigato(
        string memory comment,
        address to,
        uint256 amount
    ) public returns (bool)
```
と
```
event Arigato(
    address indexed sender,
    address indexed receiver,
    uint256 amount,
    string comment
);
```
が定義されています。

Dapps は、次の index.html だけです。
```
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Okojo Arigato</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <!-- なんとなく綺麗なレイアウトにする -->
    <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css" />
    <!-- web3.js を読み込む -->
    <script src="https://cdn.jsdelivr.net/npm/web3@1.9.0/dist/web3.min.js"></script>
  </head>
  <body>

    <!-- Login 前のみ表示 -->

    <!-- Login 後 -->

    <!-- アカウントアドレス表示部 -->

    <!-- OKJC (ERC20) 残高表示部 -->

    <!-- OKJC (ERC20) 承認額表示部、承認金額変更 -->

    <!-- OKJC (ERC20) ありがとう送信部 -->

    <!-- OKJC (ERC20) ありがとう受け取り一覧部(テーブル) -->

    <script type="text/javascript">
      // ここにスクリプトを直打ちする
      // 認証状態
      async function getLoginStatus() {

      }

      // ログインする
      async function login() {}

      // ---- Action 系 ----
      // 委譲する
      async function approveOkojo(amount) {}

      // ありがとうを送る
      async function sendArigato(to, amount, message) {}

      // ---- 表示系 -----
      // ありがとうを取得する
      async function getArigatoes() {}

      // ログイン状態を取得する
      async function getAccounts() {}

      // 委譲されたオコジョコインの総額を取得する
      async function getAllowance() {}

      // オコジョコインの残高を取得する
      async function getOkojoBalance() {}
    </script>
  </body>
</html>
```

長すぎるって怒られたらどうしようかと思いましたが、飲み込んでくれました。出てきたコードが下のコードです。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Okojo Arigato</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <!-- なんとなく綺麗なレイアウトにする -->
    <link rel="stylesheet" href="<https://cdn.simplecss.org/simple.min.css>" />
    <!-- web3.js を読み込む -->
    <script src="<https://cdn.jsdelivr.net/npm/web3@1.9.0/dist/web3.min.js>"></script>
  </head>
  <body>
    <!-- Login 前のみ表示 -->
    <button id="connectBtn" onclick="login()">Connect Wallet</button>

    <!-- Login 後 -->
    <div id="loggedIn" style="display: none">
      <!-- アカウントアドレス表示部 -->
      <div>Account: <span id="accountAddress"></span></div>

      <!-- OKJC (ERC20) 残高表示部 -->
      <div>OKJC Balance: <span id="okojoBalance"></span></div>

      <!-- OKJC (ERC20) 承認額表示部、承認金額変更 -->
      <div>
        Approved OKJC: <span id="approvedOkojo"></span>
        <input id="approveAmount" type="number" />
        <button
          onclick="approveOkojo(document.getElementById('approveAmount').value)"
        >
          Approve
        </button>
      </div>

      <!-- OKJC (ERC20) ありがとう送信部 -->
      <div>
        Send Arigato:
        <input id="sendTo" placeholder="To Address" />
        <input id="sendAmount" type="number" placeholder="Amount" />
        <input id="sendMessage" placeholder="Message" />
        <button
          onclick="sendArigato(document.getElementById('sendTo').value, document.getElementById('sendAmount').value, document.getElementById('sendMessage').value)"
        >
          Send
        </button>
      </div>

      <!-- OKJC (ERC20) ありがとう受け取り一覧部(テーブル) -->
      <table id="arigatoList">
        <tr>
          <th>From</th>
          <th>Amount</th>
          <th>Message</th>
        </tr>
      </table>
    </div>

    <script type="text/javascript">
          const OKJO_TOKEN_ADDRESS = "0x..."; // OKJCのトークンアドレスを指定してください。
          const OKJO_ARIGATO_ADDRESS = "0x..."; // OkojoArigatoのコントラクトアドレスを指定してください。
          const OKJO_ABI = [...]; // OKJCのABIを指定してください。
          const OKJO_ARIGATO_ABI = [...]; // OkojoArigatoのABIを指定してください。
          let web3;
          let okjoTokenContract;
          let okjoArigatoContract;
          let userAddress;

          // 認証状態
          async function getLoginStatus() {
            if (window.ethereum) {
              try {
                web3 = new Web3(window.ethereum);
                await window.ethereum.request({ method: "eth_requestAccounts" });
                okjoTokenContract = new web3.eth.Contract(OKJO_ABI, OKJO_TOKEN_ADDRESS);
                okjoArigatoContract = new web3.eth.Contract(OKJO_ARIGATO_ABI, OKJO_ARIGATO_ADDRESS);
                const accounts = await getAccounts();
                if (accounts.length > 0) {
                  userAddress = accounts[0];
                  return true;
                }
              } catch (error) {
                console.error("Error connecting wallet:", error);
              }
          }
          return false;
        }

      // ログインする
      async function login() {
        if (await getLoginStatus()) {
          document.getElementById("connectBtn").style.display = "none";
          document.getElementById("loggedIn").style.display = "block";
          document.getElementById("accountAddress").innerText = userAddress;

          await updateUI();
        } else {
          alert("Please install MetaMask or connect your wallet.");
        }
      }

      // UIを更新する
      async function updateUI() {
        await getOkojoBalance();
        await getApprovalCoins();
        await getArigatoes();
      }

      // 委譲する
      async function approveOkojo(amount) {
        try {
          const weiAmount = web3.utils.toWei(amount, "ether");
          await okjoTokenContract.methods.approve(OKJO_ARIGATO_ADDRESS, weiAmount).send({ from: userAddress });
          await updateUI();
        } catch (error) {
          console.error("Error approving Okojo:", error);
        }
      }

      // ありがとうを送る
      async function sendArigato(to, amount, message) {
        try {
          const weiAmount = web3.utils.toWei(amount, "ether");
          await okjoArigatoContract.methods.sendArigato(message, to, weiAmount).send({ from: userAddress });
          await updateUI();
        } catch (error) {
          console.error("Error sending Arigato:", error);
        }
      }

      // ありがとうを取得する
      async function getArigatoes() {
        try {
          const arigatoEvents = await okjoArigatoContract.getPastEvents("Arigato", {
            filter: { receiver: userAddress },
            fromBlock: 0,
            toBlock: "latest",
          });

          const arigatoList = document.getElementById("arigatoList");
          arigatoList.innerHTML = `
            <tr>
              <th>From</th>
              <th>Amount</th>
              <th>Message</th>
            </tr>
          `;

          arigatoEvents.forEach((event) => {
            const { sender, receiver, amount, comment } = event.returnValues;
            const newRow = arigatoList.insertRow(-1);
            newRow.insertCell(0).innerText = sender;
            newRow.insertCell(1).innerText = web3.utils.fromWei(amount, "ether");
            newRow.insertCell(2).innerText = comment;
          });
        } catch (error) {
          console.error("Error getting Arigatoes:", error);
        }
      }

      // ログイン状態を取得する
      async function getAccounts() {
        try {
          const accounts = await web3.eth.getAccounts();
          return accounts;
        } catch (error) {
          console.error("Error getting accounts:", error);
        }
      }

      // オコジョコインの残高を取得する
      async function getOkojoBalance() {
        try {
          const balance = await okjoTokenContract.methods.balanceOf(userAddress).call();
          document.getElementById("okojoBalance").innerText = web3.utils.fromWei(balance, "ether");
        } catch (error) {
          console.error("Error getting Okojo balance:", error);
        }
      }

      // 初回読み込み時にログイン状態をチェックしてUIを更新
      window.addEventListener("DOMContentLoaded", async () => {
        if (await getLoginStatus()) {
          document.getElementById("connectBtn").style.display = "none";
          document.getElementById("loggedIn").style.display = "block";
          document.getElementById("accountAddress").innerText = userAddress;
          await updateUI();
        }
      });

      // アカウントが切り替わったときにUIを更新
      window.ethereum.on("accountsChanged", async (accounts) => {
        userAddress = accounts[0];
        await updateUI();
      });
    </script>
  </body>
</html>

大体狙い通りになっていそうです。読むのが面倒なので早速使ってみましょう。定数で与えているコントラクトアドレスと ABI は、変更の必要があるので変更しておいてください。OKOJOって教えたつもりですが、OKJOになってたりとか、ちょいちょいおかしいのはありますが、動きそうなコードです。恐るべし、ChatGPT。

コードを読み込むのが面倒なので、サクッと読んだらウェブサーバーを立ち上げて実験してみましょう。

$ npx http-server ./public

これでウェブサーバーが立ち上がるので、 [<http://127.0.0.1:8080>](<http://127.0.0.1:8080>) にアクセスします。

ログイン画面

いきなりログインを求められますね。キャンセルをクリックして、とりあえず無視します。

そうすると激渋の画面が残りますので、もう一度、Connect Wallet をクリックします。

もう一度、ログイン画面が立ち上がってくるので、hardhat のローカルのアカウントを接続します。

接続確認画面

接続確認画面が出ますので、接続してください。ログイン後の画面が表示されました。

ログイン後の画面

Approved OKJC の中が空になっているので、具体的な値、例えば、初期であれば 0 が入っていて欲しいなと思いますが、そこはグッと堪えて、とりあえず、10OKJCをapproveしてみます。OkojoCoin をデプロイしたアカウントなので、1兆 OKJC がミントされています。

10と入れても、再度 Metamask の方でも確認画面が出ますね。ここで、10 OKJC としておきましょう。

トランザクションは成功しましたが、エラーになりました。

ブラウザのコンソールに表示されたエラー

getAllowance が定義されていないというエラーですね。だから 0 が表示されていなかったのか。ChatGPT でやるとこういうのもちょいちょい出てきますが、確かに定義されていませんので、実装します。

// 委譲可能な残高を取得する
async function getAllowance() {
  try {
    const allowance = await okjoTokenContract.methods
      .allowance(userAddress, OKJO_ARIGATO_ADDRESS)
      .call();
    document.querySelector("#approveAmount").value = web3.utils.fromWei(
      allowance,
      "ether"
    );
  } catch (error) {
    console.error("Error getting allowance:", error);
  }
}

これを approve メソッドの上の辺に挿入しておきます。

再度、実行してみましょう。これでエラーが消え承認金額がインプットに入るようになりました。

ありがとうを送信する

別のテストアカウントをインポートしてありがとうを送信してみます。まずは、インポートしたアカウントを接続しておきます。手順は同じです。

次に Metamask でアカウントを切り替えてみましょう。

新たにインポートしたアカウントでは、OKJC も Approved OKJC も 0 が表示されています。

新しいアカウント

デプロイに使ったアカウントに Metamask で切り替えてみます。すると即座に、接続されているアカウント情報が切り替わりました。

元のアカウント

これは、Metamask のイベントリスナー accountsChanged がリスンされ、 updateUI()を実行しているからです。web3 に限らずですが、event をうまく使うと使いやすいアプリになります。このイベントは、ChatGPT が気を利かせて(?)作ってくれましたね。

// アカウントが切り替わったときにUIを更新
window.ethereum.on("accountsChanged", async (accounts) => {
  userAddress = accounts[0];
  await updateUI();
});

ありがとうを送信してみる

新しくインポートしたアカウントへ、元のアカウントから100OKJC をメッセージとともに送信してみます。

Send ボタンをクリックすると確認画面が出ますので、コンファームします。

すると、OKJCバランスと委譲金額が即座に変更されました。委譲金額は、消費して減るので、減ったら追加委譲が必要になります。

一方で、ありがとう一覧は更新されていません。

ありがとう送信

この部分のコードを見てみましょう。

const arigatoEvents = await okjoArigatoContract.getPastEvents(
  "Arigato",
  {
    filter: { receiver: userAddress },
    fromBlock: 0,
    toBlock: "latest",
  }
);

ありがとう一覧は、 getPastEvents を呼び出して取得しています。 getPastEvents は、第一引数にイベント名を文字列で与えて、第二引数にオプションを設定します。よくあるオプションは、filter, fromBlock, toBlock です。filter は、検索条件、fromBlock は、どこから?toBlockは、どこまで?なので、fromBlock=0, toBlock=”latest” とすれば、すべてのブロックから取得してきます。filter のところを見てみましょう。検索条件は、receiver=userAddress となっているので、自分が受け取ったありがとう一覧が表示されるようになっています。filter の条件に入れられるものは、Event の定義で indexed が付与されいている項目のみです。

試しに、送信先のアカウントに変更してみましょう。

新たにインポートしたアカウント

表示されていいます。

ありがとうを受信する

先ほどはありがとうを送信しましたが、ありがとうを受信してみます。受信する側のアカウントで何が起こるかみてみましょう。

accountsChangedイベントが発火するので、同じコンテクストのブラウザでは接続ができません。インコグニトか、他のChromeアカウントを使うかで試してみてください。わかりやすいように、左側にデプロイのアドレス、右側に新たにインポートしたアカウントを表示しています。右側のアカウントから左側のアカウントにありがとうを送信しています。

ありがとうを送信したあと、受信側のブラウザの再読み込みで表示されますが、再読み込みするまで表示されないので、受信側はすぐに気がつくことができません。これを変えたいなと思います。

Arigato イベントの発火をリアルタイムで受信するようにする

やることはシンプルです。Event を受信するイベントリスナを設定して、イベントを受信した時にデータを更新するようにします。

async function setupEventListener() {
  okjoArigatoContract.events
    .Arigato({
      filter: { receiver: userAddress }
    })
    .on("change", async (event) => {
      console.log("Arigato on change", event);
      await updateUI();
    })
    .on("data", async (event) => {
      console.log("Arigato on data", event);
      await updateUI();
    })
    .on("connected", async (subscriptionId) => {
      console.log("Arigato on connected", subscriptionId);
    })
    .on("error", console.error);
}

この関数を呼び出すところは、DOMContentLoaded で良いかと思います。

// 初回読み込み時にログイン状態をチェックしてUIを更新
window.addEventListener("DOMContentLoaded", async () => {
  if (await getLoginStatus()) {
    setupEventListener(); // <= これを追加!
    document.getElementById("connectBtn").style.display = "none";
    document.getElementById("loggedIn").style.display = "block";
    document.getElementById("accountAddress").innerText = userAddress;
    await updateUI();
  }
});

これをやって、先ほどと同じことをすると。Hardhat でエラーがたくさん出ました。Hardhat の方の問題かもしれないので、試しに Ganache という他の開発環境も立ててみましょう。

Ganache でローカルネットを構築する

とりあえず、Hardhat の開発プロジェクトフォルダはそのまま使いましょう。検索すると、Ganache でローカルネットを構築できるっていう情報が、2秒くらいで見つかるのでやってみます。

以下から、ダウンロードしてインストールします。

Ganache – Truffle Suite

インストールしてそのまま起動、QuickStart で環境を立ち上げます。すると、先ほど hardhat でやった時のように、アカウントと秘密鍵(鍵マークアイコンクリックで表示)の一覧が出てきます。これは、便利ですね。。初めから Ganache でもいいかもしれないです。

Ganache では、ローカルネットのアカウント一覧をGUIで確認できる

Ganache のローカルネットにコントラクトをデプロイ

筆者は、サボりたがりなので、Hardhat のデプロイスクリプトをそのまま Ganache に使う方法でやりたいと思いました。とりあえず、設定アイコンをクリックして、「Server」という項目を確認してみます。Port Number と Network ID というのがあるので、これをそのまま hardhat で使っていた環境と同じにしてみます。つまり、Port Number ⇒ 8545にして、Network ID ⇒ 31337 にして再起動します。

Metamask に Ganache の環境をインポート

Metamask のネットワークを追加から、Ganache のネットワークを追加します。ネットワークIDを入力するところで、31337 を入れると、ネットワークは、1337 を返却してきているというエラーが出るので、Ganache の設定も Metamask の設定も 1337 に合わせておきます。Ganache の設定値が効いてないように感じるのは疑問ですが、とりあえず 1337 に変更してこのまま進めます。

Metamask が設定を自動的に確認する

アクティビティタブのデータを消去

とりあえず、何も考えずに、Hardhat のスクリプトを Ganache のローカルネットに適用してみます。開発環境では、キャッシュやその他のローカルデータが邪魔することがあるので、ネットワークを再起動したり別のところに接続したりしたときは、「アクティビティタブのデータ消去」を実行しておきましょう。

設定を呼び出す
高度な設定をクリック
アクティビティタブを消去します

何も問題がない時に、アクティビティを消去しても特に目に見える問題は発生しなかったので、少なくともテスト用のアカウントであれば実行して大丈夫だと思います。

スマートコントラクトをデプロイします

ネットワークIDの問題があるのでちょっと不安ですが、そのまま hardhat のスクリプトを実行します。

  • OkojoCoinをデプロイして、コントラクトIDを記録
  • OkojoArigatoのデプロイスクリプトにOkojoCoinのコントラクトIDを入れる
  • OkojoArigatoをデプロイして、コントラクトIDを記録
  • index.html 内の、コントラクトアドレスを、OkojoCoin と OkojoArigato の Ganache でのアドレスに変更

これらをやって行きます。

まずコインのデプロイですが、次のコマンドを実行します。

$ npx hardhat run ./scripts/deployCoin.js --network localhost
Contract with 0.0ETH and deployed to 0x4b9aE4398e6B5fc320986482cE2b2a3E6ecFF43D

というわけで、 0x4b9aE4398e6B5fc320986482cE2b2a3E6ecFF43D にデプロイされたようです。Ganache の画面を確認します。Hardhat と Ganache という違うもの組み合わせましたが、なんか一発でうまく行きました笑

Blocks というメニューを開くと、トランザクションが発生したことがわかります。

Block 1 を確認すると、コントラクトが作成されたことがわかります。コントラクツタブは、開いても何もなかったので、Ganache 側では認識されないようです。

Metamask でも確認してみると、Ganache 1 のテストアカウントのETH が、100より小さくなっているので、ガス代が支払われたということがわかります。

続いて、 0x4b9aE4398e6B5fc320986482cE2b2a3E6ecFF43D のコインのアドレスにスクリプトを書き換えた(読者の皆さんは、先ほどデプロイしたコインのアドレスに置き換えてください) deployArigato.js を実行します。

$ npx hardhat run ./scripts/deployArigato.js --network localhost
Contract with 0.0ETH and deployed to 0xC5ed82A9426A8b8d5b57a34faA7A87A014736635

できたっぽいです。 0xC5ed82A9426A8b8d5b57a34faA7A87A014736635 にデプロイされました。

index.html のスクリプトタグ内で読み込んでいるスマートコントラクトのアドレスを書き換えましょう。

アカウントを接続する

左側のWindowは、OKJCをミントしたアカウントになります。右側は、それ以外のアカウントです。Metamask が混乱するので、Metamask を起動して接続を確認しましょう。

二つのアカウントで確認する

Account 1 から、Account 2 へありがとうを送信

  • Account 1 で、OKJCの送信を委譲
  • Account 1 で、OKJCとコメントを Account 2 に送信

この作業で、Account 2(右側のウィンドウ)の方で、タイムラグはありますが、Eventを取得して、受け取りありがとう一覧が更新されていることがわかると思います。

左側のウィンドウでありがとうを送信した後に、右側のウィンドウで OKJC を承認しようとしたところ、Eventが発火し、OKJC 承認額が 0 になり、一番したのメッセージ一覧にメッセージが表示されたことがわかります。OKJC 承認額が 0 になったのは、 updateUI() のなかで、 getAllowance が実行されて現在の値である 0 で上書きされたからです。操作中のフォームがクリアされるのはアプリとしてイマイチなので、改善できる方は改善してみてください。

Account 2 から、Account 1 へありがとうを送信

今度は、Account 2 で同じことをします

  • Account 2 で、OKJCの送信を委譲
  • Account 2 で、OKJCとコメントを Account 1 に送信

この場合も、イベント駆動で画面が更新されているのがわかると思います。

逆の送信もうまくいきました。これでほぼ完成と言えるものができたので、これをテストネットにデプロイします。

OkojoArigato スマートコントラクトをテストネットにデプロイする

Goerli テストネットが、どうも具合悪いので、Sepolia でもなく、まさかの Mumbai にデプロイします。OkojoCoin は、すでに Goerli にデプロイされていますが、テストネットなので捨て去ってしまいましょう。Mumbai などのテストネットで、テスト用のコインを取得する方法が初の方は、前回のブログを参照してみてください。

そして、Hardhat で華麗にデプロイ・・・・ではなく、Remix IDE に戻ってデプロイします。

OkojoCoin を Mumbai Testnet にデプロイ

Remix IDE を思い出してください。コンパイルして、デプロイするときに、Environment という項目があり、Injected Provider にすれば、Metamask で選択されているプロバイダーば選択されます。

気軽にデプロイしてみましょう。

コントラクトアドレスは、 0x63F2df9F6bB880986a35e190Aba9221eC884DfF0 でした。

OkojoArigato を Mumbai Testnet にデプロイ

次に、OkojoArigato をデプロイします。コンパイルするところはそのままですが、デプロイ時に OkojoCoin との依存関係を設定する必要があるので注意しましょう。

おもむろにデプロイします。0x78220CB4eD2aEb0312496f51F496ADD3605E0243 にデプロイされました。

ローカルの画面でテスト

先ほどのウェブ画面でテストしてみましょう。変更が必要なところは、それぞれのコントラクトアドレスの部分だけかと思います。

デプロイに使ったテストアカウントで、OKJC残高を確認しました。

Mumbai にデプロイできたことを確認。

ありがとう送信も試します。送金を委譲することを忘れずに10万OKJCを承認しておきます。そして、1000OKJCとコメントを送金します。

Mumbai で Arigato の送信をテスト

受け取り側のアカウントを確認すると、確かに、OKJCの残高が増えており受け取ったことを確認できますが、ありがとう一覧は確認できません。

コンソールでエラーを確認すると、次のエラーが出ていました。

Blockheight too far というのは、fromBlock から toBlock までが、1000ブロックまでにしてくれよっていうエラーです。今までは、ローカルのVMやRemix VM でテストしていたので、 ネットワークのブロックは、たかだか10個程度しかありませんでした。

しかし、実際のチェーンでは、1000を軽く超えるブロックが登録されています。なので、 getPastEvents()の実装を見直しましょう。

具体的には、遡るブロックの数を1,000より十分小さくしました。

// 適当な場所にこちらを実装しましょう、
// 最大のfromBlockという意味ですが、英語の命名的にイマイチな気がします
async function getMaxFromBlock() {
  const MAX_BLOCKS = 999;
  const blockNumber = await web3.eth.getBlockNumber();
  let fromBlock = 0;
  if (blockNumber > MAX_BLOCKS){
    fromBlock = blockNumber - MAX_BLOCKS;
  }
  return fromBlock;
}

// getArigatoes 内
const arigatoEvents = await okjoArigatoContract.getPastEvents(
  "Arigato",
  {
    filter: { receiver: userAddress },
    fromBlock: (await getMaxFromBlock()),
    toBlock: "latest",
  }
);

from と to でパジネーションの仕組みを入れることで制限を回避するとともに、各種ノードの負荷も低減されると思います。MAX_BLOCKS は、エラーメッセージでは、1,000が限界ということだったので、999 までとることはできそうという意図です。途中で、ネットワークのブロックが増えている可能性もあるので、バッファはある程度見積もった方が良いかもです。

試してみます。

狙い通りエラーが消えました。

しかし、イベントがうまく発火していない感じです。それはまた別で調査します。。。

スマートコントラクトとフロントエンド

一度、シンプルなスマートコントラクトをデプロイすればあとはフロントエンドの改良だけでアプリが改良できます。ただし反面、フロントエンドに重要な機能を持たせないように注意してください。スマートコントラクトは、ネットワーク常にデプロイされているので、あなたが作成したアプリ以外の別の方法でも利用することができます。フロントエンドに重要な機能や処理を持たせてしまうと、スマートコントラクト自体は整合性を保ちますが、フロントエンドアプリの整合性を保つのは困難になると思います。これはスマートコントラクトに限った話ではなく、通常のWeb開発でも同様かと思います。

Web インターフェースを firebase にデプロイ

さて、アプリケーションをデプロイして公開ましょう。ここでやることは、index.html をホスティングするだけなので、どこでもいいのですが無料の firebase にデプロイしましょう。

アカウントが無い方は、下のリンクからアカウントを登録します。

Firebase

アカウントを作成したら、firebase のコマンドラインツールをインストールします。

$ npm install -g firebase-tools

firebase にログインします。

$ firebase login

指示に従ってログインします。初期化します。

$ firebase init

? Which Firebase features do you want to set up for this directory? Press Space to select features, then Enter to confirm 
your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
 ◯ Realtime Database: Configure a security rules file for Realtime Database and (optionally) provision default instance
 ◯ Firestore: Configure security rules and indexes files for Firestore
 ◯ Functions: Configure a Cloud Functions directory and its files
❯◯ Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys
 ◯ Hosting: Set up GitHub Action deploys
 ◯ Storage: Configure a security rules file for Cloud Storage
 ◯ Emulators: Set up local emulators for Firebase products
(Move up and down to reveal more choices)

? Please select an option: 
  Use an existing project 
❯ Create a new project 
  Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project

? Please specify a unique project id (warning: cannot be modified afterward) [6-30 characters]:
 okojo-arigato
? What would you like to call your project? (defaults to your project ID) 
✔ Creating Google Cloud Platform project

あとは、 public/index.html を上書きされないように気をつけながら、基本的には全部デフォルトでOKです。

デプロイします。

$ firebase deploy

=== Deploying to 'okojo-arigato'...

i  deploying hosting
i  hosting[okojo-arigato]: beginning deploy...
i  hosting[okojo-arigato]: found 2 files in public
✔  hosting[okojo-arigato]: file upload complete
i  hosting[okojo-arigato]: finalizing version...
✔  hosting[okojo-arigato]: version finalized
i  hosting[okojo-arigato]: releasing new version...
✔  hosting[okojo-arigato]: release complete

✔  Deploy complete!

Project Console: <https://console.firebase.google.com/project/okojo-arigato/overview>
Hosting URL: <https://okojo-arigato.web.app>

これで完了です。https://okojo-arigato.web.app にアクセスすれば、アプリが利用できます。

アプリケーションの課題と修正案

テストネット上ではあるものの、これで初めての Dapps の公開ができました。このアプリケーションにもいくつかの課題があります。パッと思いつくところを以下に列挙します。

  1. getPastEvents は、1000ブロック手前までしか検索してこないので全件の履歴が取れない
  2. 送信者名がアカウントアドレスになっていて、正直誰だかわからない
  3. 言語が英語になっていて日本語の話者にとってはハードルを感じる
  4. いつ送信されたありがとうなのかわからない
  5. Metamask の利用、Mumbai MATIC の所有、OKJC の所有が必要

1、については、スキャンを繰り返して過去まで遡ることが可能です。ただし、パフォーマンスが非常に悪くなるので非効率です。回避策としては、スマートコントラクトに書き込んでしまうと言うのもありますが、後々ガス代が問題になるのが目に見えています。そう言う場合は、ネットワーク外のデータベースと同期することで問題を回避できる可能性があります。Mumbai Testnet の全ての取引を同期するのは容易ではありませんが、Arigato イベントだけ同期しておけば通常のウェブアプリと同程度のデータ量で済みます。これをオフチェーンを使うと言います。オフチェーン上に Arigato イベントを取得しておけば、過去のありがとう一覧や検索、パジネーションなど柔軟なアプリ開発が可能になります。ただし、オンチェーンとの同期が必要になるのでサードパーティの支援ツールを活用するなどして問題に取り組むことになります。

2、についても同様にオフチェーンの利用が考えられます。オフチェーンのデータベース上に、アカウントアドレスと個人名を記録するテーブルがあれば、わかりやすい個人名で表示させられます。また、チェーン上にはアカウントアドレスしか記録されていませんので、信頼のおけるメンバー間だけで、プライバシーを守りつつ個人名を利用するにはオフチェーンは良い選択の一つです。

3-4、については、フロントエンドアプリケーションの改良だけで対応可能です。

5 は、最も厄介な問題になります。このアプリを使ってもらうためには、Metamask をインストールして、Mumbai Faucet から、Mumbai MATIC を取得して、OKJC を筆者に依頼するなどして受け取る必要があります。OKJCについは、Mumbai MATIC と交換できるようにしておけばある程度の問題は解決できますが、Metamask のインストールと Mumbai MATIC の取得はハードルとなりそうです。後者は簡単に取得できるので、Metamask のインストールがメインのハードルでしょうか。この辺りは、web3 全体の課題とも言えます。

編集後記

筆者も同時にやりながら色々学ばせていただきました。このアプリを実際に利用するには、OKJCが必要になるので、@shr_f にメンションいただけたらERC20トークンを送金します。Mumbai MATIC のテストネットにデプロイしているので、それだけ気をつけてください。

あとは、プログラムのスケッチを書いたところで、外部の情報を同時に ChatGPT に入れてみたところ、少しの修正が必要になりましたが、ほぼ完成品が出来上がったのは驚愕ですね。簡単なソフトウェアは、ChatGPT で作れるようになってます。ChatGPT で生成された画面を日本語化しておけばよかったと思いたが、これはこのまま行ってよかったかなとおもいます。

オートロ株式会社では、新しいテクノロジーの発見・実験を積極的に行い、業務自動化するプラットフォーム AUTORO の開発運営をしています。興味のある方、我こそはという方は、絶賛採用中ですので、@shr_f まで連絡いただければ幸甚です。

最後に、おすすめの web3 ユーチューバーのリンクを掲載します。

https://www.youtube.com/c/joichiito

この記事を書いた人

Shiro Fukuda
Shiro Fukuda

プロフィールは準備中です。