Ethereum フロントエンドWebアプリ開発 Angular MetaMask ethers編

2021/10/20に公開

自己紹介

株式会社CauchyEのエンジニアの松岡靖典です。
ブロックチェーンの技術的な面白さに魅了され、Web開発の世界を歩み始めました。
NPO法人NEM技術普及推進会NEMTUSの理事としてブロックチェーン技術の普及推進活動に従事したり、個人開発等の取り組みも行っています。
JavaScript, TypeScript, Angular, Firebase, NEM, Symbol, Cosmosが好きですが、チャンスがあれば他にも様々な技術に触れていきたいと思っています。

記事概要要約

以前、Ethereumのコントラクト開発に関する以下の2記事を作成したのですが、記事の内容が膨れ上がりすぎてしまい、フロントエンドからMetaMaskを用いてコントラクトをデプロイしたり、コントラクトを呼び出したりする方法の説明ができませんでした。

  1. Ethereum コントラクト開発 Hello world ローカル編
  2. Ethereum コントラクト開発 Hello world テストネット編

今回の記事では、上記記事で扱ったHello world的なシンプルなコントラクトを題材に、MetaMaskを用いたデプロイやコントラクト呼び出しが可能なフロントエンドWebアプリケーションの作成方法を解説します。
SPAとしてAngular、MetaMaskとの連携やEthereum関連処理はethersを用いて実装していきます。

作成したWebアプリとそのレポジトリ

作成したWebアプリのサンプルは以下URLにて公開しています。MetaMaskが対応しているEVM互換ネットワークであれば、どのネットワークでも動くと思います。

https://metamask-sample.web.app/

作成したWebアプリのソースコードは以下レポジトリにて公開しています。必要あれば、こちらのレポジトリをご参照ください。

https://github.com/YasunoriMATSUOKA/metamask-sample

前提環境

  • OS
    • Ubuntu 20.04 (on WSL2)
  • Node.js
    • v14.17.4
  • npm
    • 7.20.5

コントラクトのおさらい

まずは今回改めて扱うコントラクトの内容を簡単におさらいしておきましょう。

contracts/contracts/NFT.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "hardhat/console.sol";


contract Greeter {
  string greeting;
  
  constructor(string memory _greeting) {
    console.log("Deploying a Greeter with greeting:", _greeting);
    greeting = _greeting;
  }
  
  function greet() public view returns (string memory) {
    return greeting;
  }
  
  function setGreeting(string memory _greeting) public {
    console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
    greeting = _greeting;
  }
}

コントラクトの機能面のポイントは以下の通りです。

  • デプロイ時には、挨拶文の文字列を指定してコントラクトをデプロイする必要がある。
  • greet関数を呼び出すと、現時点でコントラクトに設定されている挨拶文の文字列を取得できる。
  • setGreeting関数を、引数に新たな挨拶文として設定したい文字列を入れて、呼び出すことで、コントラクトに新しい挨拶文をセットできる。

コントラクトをデプロイするために必要なabiとbytecodeは、以下省略表示のjsonに含まれています。

Greeter.json
contracts/artifacts/contracts/Greeter.sol/Greeter.json
{
  "_format": "hh-sol-artifact-1",
  "contractName": "Greeter",
  "sourceName": "contracts/Greeter.sol",
  "abi": [
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_greeting",
          "type": "string"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "inputs": [],
      "name": "greet",
      "outputs": [
        {
          "internalType": "string",
          "name": "",
          "type": "string"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_greeting",
          "type": "string"
        }
      ],
      "name": "setGreeting",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ],
  "bytecode": "0x60806040523480156200001157600080fd5b5060405162000c3238038062000c32833981810160405281019062000037919062000278565b6200006760405180606001604052806022815260200162000c1060229139826200008760201b620001ce1760201c565b80600090805190602001906200007f92919062000156565b5050620004c5565b620001298282604051602401620000a0929190620002fe565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506200012d60201b60201c565b5050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b8280546200016490620003ea565b90600052602060002090601f016020900481019282620001885760008555620001d4565b82601f10620001a357805160ff1916838001178555620001d4565b82800160010185558215620001d4579182015b82811115620001d3578251825591602001919060010190620001b6565b5b509050620001e39190620001e7565b5090565b5b8082111562000202576000816000905550600101620001e8565b5090565b60006200021d620002178462000362565b62000339565b9050828152602081018484840111156200023657600080fd5b62000243848285620003b4565b509392505050565b600082601f8301126200025d57600080fd5b81516200026f84826020860162000206565b91505092915050565b6000602082840312156200028b57600080fd5b600082015167ffffffffffffffff811115620002a657600080fd5b620002b4848285016200024b565b91505092915050565b6000620002ca8262000398565b620002d68185620003a3565b9350620002e8818560208601620003b4565b620002f381620004b4565b840191505092915050565b600060408201905081810360008301526200031a8185620002bd565b90508181036020830152620003308184620002bd565b90509392505050565b60006200034562000358565b905062000353828262000420565b919050565b6000604051905090565b600067ffffffffffffffff82111562000380576200037f62000485565b5b6200038b82620004b4565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b60005b83811015620003d4578082015181840152602081019050620003b7565b83811115620003e4576000848401525b50505050565b600060028204905060018216806200040357607f821691505b602082108114156200041a576200041962000456565b5b50919050565b6200042b82620004b4565b810181811067ffffffffffffffff821117156200044d576200044c62000485565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b61073b80620004d56000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae321714610057575b600080fd5b6100556004803603810190610050919061043d565b610075565b005b61005f61013c565b60405161006c91906104b7565b60405180910390f35b6101226040518060600160405280602381526020016106e3602391396000805461009e90610610565b80601f01602080910402602001604051908101604052809291908181526020018280546100ca90610610565b80156101175780601f106100ec57610100808354040283529160200191610117565b820191906000526020600020905b8154815290600101906020018083116100fa57829003601f168201915b50505050508361026a565b8060009080519060200190610138929190610332565b5050565b60606000805461014b90610610565b80601f016020809104026020016040519081016040528092919081815260200182805461017790610610565b80156101c45780601f10610199576101008083540402835291602001916101c4565b820191906000526020600020905b8154815290600101906020018083116101a757829003601f168201915b5050505050905090565b61026682826040516024016101e49291906104d9565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b5050565b61030483838360405160240161028293929190610510565b6040516020818303038152906040527f2ced7cef000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b505050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b82805461033e90610610565b90600052602060002090601f01602090048101928261036057600085556103a7565b82601f1061037957805160ff19168380011785556103a7565b828001600101855582156103a7579182015b828111156103a657825182559160200191906001019061038b565b5b5090506103b491906103b8565b5090565b5b808211156103d15760008160009055506001016103b9565b5090565b60006103e86103e384610581565b61055c565b90508281526020810184848401111561040057600080fd5b61040b8482856105ce565b509392505050565b600082601f83011261042457600080fd5b81356104348482602086016103d5565b91505092915050565b60006020828403121561044f57600080fd5b600082013567ffffffffffffffff81111561046957600080fd5b61047584828501610413565b91505092915050565b6000610489826105b2565b61049381856105bd565b93506104a38185602086016105dd565b6104ac816106d1565b840191505092915050565b600060208201905081810360008301526104d1818461047e565b905092915050565b600060408201905081810360008301526104f3818561047e565b90508181036020830152610507818461047e565b90509392505050565b6000606082019050818103600083015261052a818661047e565b9050818103602083015261053e818561047e565b90508181036040830152610552818461047e565b9050949350505050565b6000610566610577565b90506105728282610642565b919050565b6000604051905090565b600067ffffffffffffffff82111561059c5761059b6106a2565b5b6105a5826106d1565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b838110156105fb5780820151818401526020810190506105e0565b8381111561060a576000848401525b50505050565b6000600282049050600182168061062857607f821691505b6020821081141561063c5761063b610673565b5b50919050565b61064b826106d1565b810181811067ffffffffffffffff8211171561066a576106696106a2565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fe4368616e67696e67206772656574696e672066726f6d202725732720746f2027257327a2646970667358221220d8b702fd211fe2829242f362fda35c636e2590000ac3fa2b44f0ac2fb1183cd564736f6c634300080400334465706c6f79696e67206120477265657465722077697468206772656574696e673a",
  "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae321714610057575b600080fd5b6100556004803603810190610050919061043d565b610075565b005b61005f61013c565b60405161006c91906104b7565b60405180910390f35b6101226040518060600160405280602381526020016106e3602391396000805461009e90610610565b80601f01602080910402602001604051908101604052809291908181526020018280546100ca90610610565b80156101175780601f106100ec57610100808354040283529160200191610117565b820191906000526020600020905b8154815290600101906020018083116100fa57829003601f168201915b50505050508361026a565b8060009080519060200190610138929190610332565b5050565b60606000805461014b90610610565b80601f016020809104026020016040519081016040528092919081815260200182805461017790610610565b80156101c45780601f10610199576101008083540402835291602001916101c4565b820191906000526020600020905b8154815290600101906020018083116101a757829003601f168201915b5050505050905090565b61026682826040516024016101e49291906104d9565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b5050565b61030483838360405160240161028293929190610510565b6040516020818303038152906040527f2ced7cef000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b505050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b82805461033e90610610565b90600052602060002090601f01602090048101928261036057600085556103a7565b82601f1061037957805160ff19168380011785556103a7565b828001600101855582156103a7579182015b828111156103a657825182559160200191906001019061038b565b5b5090506103b491906103b8565b5090565b5b808211156103d15760008160009055506001016103b9565b5090565b60006103e86103e384610581565b61055c565b90508281526020810184848401111561040057600080fd5b61040b8482856105ce565b509392505050565b600082601f83011261042457600080fd5b81356104348482602086016103d5565b91505092915050565b60006020828403121561044f57600080fd5b600082013567ffffffffffffffff81111561046957600080fd5b61047584828501610413565b91505092915050565b6000610489826105b2565b61049381856105bd565b93506104a38185602086016105dd565b6104ac816106d1565b840191505092915050565b600060208201905081810360008301526104d1818461047e565b905092915050565b600060408201905081810360008301526104f3818561047e565b90508181036020830152610507818461047e565b90509392505050565b6000606082019050818103600083015261052a818661047e565b9050818103602083015261053e818561047e565b90508181036040830152610552818461047e565b9050949350505050565b6000610566610577565b90506105728282610642565b919050565b6000604051905090565b600067ffffffffffffffff82111561059c5761059b6106a2565b5b6105a5826106d1565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b838110156105fb5780820151818401526020810190506105e0565b8381111561060a576000848401525b50505050565b6000600282049050600182168061062857607f821691505b6020821081141561063c5761063b610673565b5b50919050565b61064b826106d1565b810181811067ffffffffffffffff8211171561066a576106696106a2565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fe4368616e67696e67206772656574696e672066726f6d202725732720746f2027257327a2646970667358221220d8b702fd211fe2829242f362fda35c636e2590000ac3fa2b44f0ac2fb1183cd564736f6c63430008040033",
  "linkReferences": {},
  "deployedLinkReferences": {}
}

いずれの内容も、詳細は前回記事(https://zenn.dev/cauchye/articles/ethereum-contract-helloworld-local)にて説明してありますので、必要に応じてそちらをご参照ください。

Angularプロジェクト作成

まずはAngular CLIをインストールしましょう。Angularの詳細は必要に応じて公式ページ(https://angular.jp/)等を参考にしてください。

npm install -g @angular/cli

Angular CLIがインストールできたら、以下コマンドを実行して、Angularアプリの雛型を作成しましょう。

ng new metamask-sample

雛型が作成されたら、以下コマンドを実行して、雛型アプリがhttp://localhost:4200で起動されるか確認しましょう。

cd metamask-sample
ng serve --open

Angularの雛型アプリが起動していることが確認できたら、デフォルトのコンテンツは全て削除して、router-outletだけにしておきましょう。

なお、この記事の中の説明では、特に記載なき場合、ファイルパスやディレクトリのパス等の表記は、プロジェクトのルートディレクトリを基準に記載しておきます。

src/app/app.component.html
<router-outlet></router-outlet>

tailwind, daisyUIの設定

tailwindの設定の詳細説明は省略させて頂きます。手順は以下リンクのページを参考にさせて頂きました。

https://blog.suzukishouten.co.jp/archives/2330

tailwindの公式ドキュメントは以下リンクをご参照ください。

https://tailwindcss.com/docs/installation

daisyUIの設定の詳細説明も省略させて頂きます。手順は以下の公式ページをご参照ください。

https://daisyui.com/docs/install

ホームページ用コンポーネント作成、ルーティング設定

次に、/でアクセスした際に表示されるコンポーネントを作成していきましょう。

プロジェクトのルートディレクトリで以下コマンドを実行して親コンポーネント、子コンポーネントを作成し、

ng g component pages/home
ng g component views/view-home

親コンポーネントに子コンポーネントが表示されるよう、親コンポーネントのhtmlファイルに子コンポーネントのタグを以下のように追記し、

src/app/pages/home/home.component.html
<p>home works!</p>
<app-view-home></app-view-home>

ルーティング設定ファイルを以下のように修正します。

src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

ここまで実行できたら、ng serveしてhttp://localhost:4200/にアクセスすると、以下のようなページが表示される状態になると思います。

home works!
view-home works!

ethersのインストール

実装すべきコンポーネントの準備ができたので、次に、MetaMaskと連携するのに使用するethersをインストールします。
インストール方法は以下リンクを参考にしてください。

https://docs.ethers.io/v5/getting-started/#installing

具体的には以下コマンドを実行すればOKです。

npm install --save ethers

ethersの仕組みの概略としては、Provider, Signer, Contract3要素が重要だと思います。

MetaMaskとの連携の際には、以下のような流れでそれらを利用することになるので、それぞれの方法を押さえておくと良いでしょう。

  • まずはインストールしたethersをインポートします。インポート方法はhttps://docs.ethers.io/v5/getting-started/#importingが参考になるでしょう。
  • MetaMaskがブラウザのwindowに対してwindow.ethereumを提供されていれば、ethersを用いて以下のようにProvider, Signerを定義できます。https://docs.ethers.io/v5/getting-started/#getting-started--connecting
    • Providerは接続先ネットワークの情報や、接続先ノードに関連する処理をコントロールしてくれるものと理解しておくとわかりやすいと思います。
    • Signerは秘密鍵を管理して必要に応じて署名を行う処理をコントロールしてくれるものと理解しておくとわかりやすいと思います。

MetaMaskへの接続をリクエストする

ここまで説明した内容で既にアプリを実装できそうですが、注意すべきことが一点あります。

MetaMaskは、誤解を恐れず一言で説明すると、ブラウザのwindowオブジェクトにethereumというオブジェクトをセットすることで、Webアプリがそのwindow.ethereumオブジェクトを利用してEthereumブロックチェーンの操作することを可能にしています

しかし、どのようなWebアプリのwindowオブジェクトにも、無条件でwindow.ethereumをセットしていると、ユーザーのアドレスが無条件に奪われてしまう等、セキュリティ的に望ましくない事態が引き起こされてしまいかねません。

そのため、MetaMaskでは、初めて表示されたWebページでは、デフォルトではwindow.ethereumをセットしない設定に今はなっていると思います。

このため、MetaMaskに対し、Webアプリ側からwindow.ethereumをセットしてほしい旨のリクエストを送り、ユーザーのMetaMask上での許可操作を通じて許可が得られて初めてwindow.ethereumがセットされるという流れになります。

もちろん、ユーザー自身で、このWebアプリに接続許可を明示的に与えようということで、手動でMetaMaskで設定することも可能なのですが、Webアプリ側がハンドリングして誘導してあげたほうが親切だと思うのでWebアプリ側でそこも実装したほうが良いでしょう。

具体的な方法は以下リンクが参考になると思います。

https://docs.metamask.io/guide/getting-started.html#connecting-to-metamask

ethersを用いたMetaMask連携の実装

以上で実装に必要な前提情報がそろったと思うので、Angularのコンポーネント上にサンプルとなるコードを実装していきます。

今回のサンプルアプリでは、以下4機能を実装していきます。

  1. MetaMaskへの接続のリクエスト
  2. コントラクトのデプロイ
  3. コントラクトの関数の呼び出し(トランザクション発行不要なもの)
  4. コントラクトの関数の呼び出し(トランザクション発行必要なもの)

実装後のAngularの各コンポーネントのファイルや、コントラクトのabiやbytecodeをインポートするためのファイル等は以下の通りとなります。詳細は以下コードを参考にしてください。

親コンポーネントhtmlファイル

src/app/pages/home/home.component.html
<app-view-home
  [currentAccount]="currentAccount"
  [currentNetwork]="currentNetwork"
  [contract]="contract"
  [deployTransaction]="deployTransaction"
  [greets]="greets"
  (connectToMetaMask)="appConnectToMetaMask()"
  (deployGreeterContract)="appDeployGreeterContract()"
  (callGreetFunction)="appCallGreetFunction()"
  (callSetGreetingFunction)="appCallSetGreetingFunction($event)"
></app-view-home>

親コンポーネントTypeScriptファイル

src/app/pages/home/home.component.ts
import { Component, OnInit } from '@angular/core';
import { ethers } from 'ethers';
import { greeterContract } from '../../models/contracts/greeter/greeter'

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
  abi = greeterContract.abi;
  bytecode = greeterContract.bytecode;
  isEthereumReady?: boolean;
  isMetaMask?: boolean;
  isConnectedToMetaMask?: boolean;
  accounts?: any[];
  currentAccount?: string;
  currentNetwork?: string;
  provider?: ethers.providers.Web3Provider;
  signer?: ethers.providers.JsonRpcSigner;
  contractFactory?: ethers.ContractFactory;
  contract?: ethers.Contract;
  contractAddress?: string;
  deployTransaction?: ethers.providers.TransactionResponse;
  greets: string[];
  greetingMessage?: string;
  setGreetingTransaction?: ethers.providers.TransactionResponse;

  constructor() {
    this.greets = [];
  }

  ngOnInit(): void { }

  async appConnectToMetaMask(): Promise<void> {
    const ethereum = (window as any).ethereum
    console.log('ethereum', ethereum);
    this.isEthereumReady = typeof ethereum === undefined;
    if (this.isEthereumReady) {
      console.error('MetaMask is not installed!');
      return;
    }
    this.isMetaMask = ethereum.isMetaMask;
    if (!this.isMetaMask) {
      console.error('It is not MetaMask!');
      return;
    }
    try {
      this.accounts = await ethereum.request({ method: 'eth_requestAccounts' });
      console.log('accounts', this.accounts);
    } catch (error) {
      console.error('Failed to get MetaMask Accounts!');
      console.error(error);
      return;
    }
    if (this.accounts === undefined || this.accounts.length === 0) {
      console.error('MetaMask does not have any accounts!');
      return;
    }
    try {
      this.currentAccount = (window as any).ethereum.selectedAddress;
      console.log('currentAccount', this.currentAccount);
    } catch (error) {
      console.error(error);
      console.error('Failed to select MetaMask 1st Account!');
      return;
    }
    this.currentNetwork = (window as any).ethereum.networkVersion;
  }

  async appDeployGreeterContract(): Promise<void> {
    await this.appConnectToMetaMask();
    this.provider = new ethers.providers.Web3Provider((window as any).ethereum);
    this.signer = this.provider.getSigner();
    this.contractFactory = new ethers.ContractFactory(this.abi, this.bytecode, this.signer);
    this.contract = await this.contractFactory.deploy("Hello, world!");
    console.log('contract', this.contract);
    this.contractAddress = this.contract.address;
    console.log('contractAddress', this.contractAddress);
    this.deployTransaction = this.contract.deployTransaction;
    console.log('deployTransaction', this.deployTransaction);
    await this.contract.deployTransaction.wait(1);
    console.log('deployTransaction is 1 confirmed');
  }

  async appCallGreetFunction(): Promise<void> {
    if (this.contract === undefined && this.contractAddress !== undefined && this.provider !== undefined) {
      this.contract = new ethers.Contract(this.contractAddress, this.abi, this.provider);
    }
    this.greetingMessage = await this.contract?.greet();
    console.log('greetingMessage', this.greetingMessage);
    if (this.greetingMessage) {
      this.greets.push(this.greetingMessage);
    }
  }

  async appCallSetGreetingFunction(newGreetingMessage: string): Promise<void> {
    if (this.contract === undefined && this.contractAddress !== undefined && this.provider !== undefined) {
      this.contract = new ethers.Contract(this.contractAddress, this.abi, this.provider);
    }
    this.setGreetingTransaction = await this.contract?.setGreeting(newGreetingMessage);
    console.log('setGreetingTransaction', this.setGreetingTransaction);
    await this.setGreetingTransaction?.wait(1);
    console.log('setGreetingTransaction is 1 confirmed');
  }
}

子コンポーネントhtmlファイル

src/app/views/view-home/view-home.component.html
<h1 class="text-5xl">Home</h1>

<div class="card lg:card-side bordered">
  <div class="card-body">
    <h2 class="card-title">Connect to MetaMask</h2>
    <div class="break-all">Current Address: {{ currentAccount }}</div>
    <div class="break-all">Current Network: {{ currentNetwork }}</div>
    <div class="card-actions">
      <button class="btn btn-primary" (click)="onConnectToMetaMask()">Connect</button>
    </div>
  </div>
</div>

<div class="card lg:card-side bordered">
  <div class="card-body">
    <h2 class="card-title">Deploy Greeter Contract</h2>
    <div class="break-all">Contract Address: {{ contract?.address }}</div>
    <div class="break-all">Deploy Transaction Hash: {{ deployTransaction?.hash }}</div>
    <div class="card-actions">
      <button class="btn btn-primary" (click)="onDeployGreeterContract()">Deploy</button>
    </div>
  </div>
</div>

<div class="card lg:card-side bordered">
  <div class="card-body">
    <h2 class="card-title">Call greet function</h2>
    <h3>Greeting Message History</h3>
    <ng-container *ngFor="let greet of greets">
      <div>{{ greet }}</div>
    </ng-container>
    <div class="card-actions">
      <button class="btn btn-primary" (click)="onCallGreetFunction()">Greet</button>
    </div>
  </div>
</div>

<div class="card lg:card-side bordered">
  <div class="card-body">
    <h2 class="card-title">Call set greeting function</h2>
    <form #formRef="ngForm" (submit)="onCallSetGreetingFunction(newGreetingMessageRef.value)">
      <div class="form-control">
        <label class="label">
          <span class="label-text">New Greeting Message</span>
        </label>
        <input
          class="input input-bordered"
          type="text"
          #newGreetingMessageRef
          name="newGreetingMessage"
          required
        />
      </div>
      <div class="card-actions">
        <button class="btn btn-primary" [disabled]="formRef.invalid">Set New Message</button>
      </div>
    </form>
  </div>
</div>

子コンポーネントTypeScriptファイル

src/app/views/view-home/view-home.component.ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ethers } from 'ethers';

@Component({
  selector: 'app-view-home',
  templateUrl: './view-home.component.html',
  styleUrls: ['./view-home.component.css']
})
export class ViewHomeComponent implements OnInit {
  @Input() currentAccount?: string;
  @Input() currentNetwork?: string;
  @Input() contract?: ethers.Contract;
  @Input() deployTransaction?: ethers.providers.TransactionResponse;
  @Input() greets?: string[];

  @Output() connectToMetaMask = new EventEmitter();
  @Output() deployGreeterContract = new EventEmitter();
  @Output() callGreetFunction = new EventEmitter();
  @Output() callSetGreetingFunction = new EventEmitter<string>();

  constructor() { }

  ngOnInit(): void { }

  onConnectToMetaMask(): void {
    this.connectToMetaMask.emit();
  }

  onDeployGreeterContract(): void {
    this.deployGreeterContract.emit();
  }

  onCallGreetFunction(): void {
    this.callGreetFunction.emit();
  }

  onCallSetGreetingFunction(newGreetingMessage: string): void {
    this.callSetGreetingFunction.emit(newGreetingMessage);
  }
}

コントラクトのabiとbytecodeが含まれたjsonをインポートして使用するためのファイル

src/app/models/contracts/greeter/greeter.ts
export const greeterContract = {
  "_format": "hh-sol-artifact-1",
  "contractName": "Greeter",
  "sourceName": "contracts/Greeter.sol",
  "abi": [
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_greeting",
          "type": "string"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "inputs": [],
      "name": "greet",
      "outputs": [
        {
          "internalType": "string",
          "name": "",
          "type": "string"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_greeting",
          "type": "string"
        }
      ],
      "name": "setGreeting",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ],
  "bytecode": "0x60806040523480156200001157600080fd5b5060405162000c3238038062000c32833981810160405281019062000037919062000278565b6200006760405180606001604052806022815260200162000c1060229139826200008760201b620001ce1760201c565b80600090805190602001906200007f92919062000156565b5050620004c5565b620001298282604051602401620000a0929190620002fe565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506200012d60201b60201c565b5050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b8280546200016490620003ea565b90600052602060002090601f016020900481019282620001885760008555620001d4565b82601f10620001a357805160ff1916838001178555620001d4565b82800160010185558215620001d4579182015b82811115620001d3578251825591602001919060010190620001b6565b5b509050620001e39190620001e7565b5090565b5b8082111562000202576000816000905550600101620001e8565b5090565b60006200021d620002178462000362565b62000339565b9050828152602081018484840111156200023657600080fd5b62000243848285620003b4565b509392505050565b600082601f8301126200025d57600080fd5b81516200026f84826020860162000206565b91505092915050565b6000602082840312156200028b57600080fd5b600082015167ffffffffffffffff811115620002a657600080fd5b620002b4848285016200024b565b91505092915050565b6000620002ca8262000398565b620002d68185620003a3565b9350620002e8818560208601620003b4565b620002f381620004b4565b840191505092915050565b600060408201905081810360008301526200031a8185620002bd565b90508181036020830152620003308184620002bd565b90509392505050565b60006200034562000358565b905062000353828262000420565b919050565b6000604051905090565b600067ffffffffffffffff82111562000380576200037f62000485565b5b6200038b82620004b4565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b60005b83811015620003d4578082015181840152602081019050620003b7565b83811115620003e4576000848401525b50505050565b600060028204905060018216806200040357607f821691505b602082108114156200041a576200041962000456565b5b50919050565b6200042b82620004b4565b810181811067ffffffffffffffff821117156200044d576200044c62000485565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b61073b80620004d56000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae321714610057575b600080fd5b6100556004803603810190610050919061043d565b610075565b005b61005f61013c565b60405161006c91906104b7565b60405180910390f35b6101226040518060600160405280602381526020016106e3602391396000805461009e90610610565b80601f01602080910402602001604051908101604052809291908181526020018280546100ca90610610565b80156101175780601f106100ec57610100808354040283529160200191610117565b820191906000526020600020905b8154815290600101906020018083116100fa57829003601f168201915b50505050508361026a565b8060009080519060200190610138929190610332565b5050565b60606000805461014b90610610565b80601f016020809104026020016040519081016040528092919081815260200182805461017790610610565b80156101c45780601f10610199576101008083540402835291602001916101c4565b820191906000526020600020905b8154815290600101906020018083116101a757829003601f168201915b5050505050905090565b61026682826040516024016101e49291906104d9565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b5050565b61030483838360405160240161028293929190610510565b6040516020818303038152906040527f2ced7cef000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b505050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b82805461033e90610610565b90600052602060002090601f01602090048101928261036057600085556103a7565b82601f1061037957805160ff19168380011785556103a7565b828001600101855582156103a7579182015b828111156103a657825182559160200191906001019061038b565b5b5090506103b491906103b8565b5090565b5b808211156103d15760008160009055506001016103b9565b5090565b60006103e86103e384610581565b61055c565b90508281526020810184848401111561040057600080fd5b61040b8482856105ce565b509392505050565b600082601f83011261042457600080fd5b81356104348482602086016103d5565b91505092915050565b60006020828403121561044f57600080fd5b600082013567ffffffffffffffff81111561046957600080fd5b61047584828501610413565b91505092915050565b6000610489826105b2565b61049381856105bd565b93506104a38185602086016105dd565b6104ac816106d1565b840191505092915050565b600060208201905081810360008301526104d1818461047e565b905092915050565b600060408201905081810360008301526104f3818561047e565b90508181036020830152610507818461047e565b90509392505050565b6000606082019050818103600083015261052a818661047e565b9050818103602083015261053e818561047e565b90508181036040830152610552818461047e565b9050949350505050565b6000610566610577565b90506105728282610642565b919050565b6000604051905090565b600067ffffffffffffffff82111561059c5761059b6106a2565b5b6105a5826106d1565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b838110156105fb5780820151818401526020810190506105e0565b8381111561060a576000848401525b50505050565b6000600282049050600182168061062857607f821691505b6020821081141561063c5761063b610673565b5b50919050565b61064b826106d1565b810181811067ffffffffffffffff8211171561066a576106696106a2565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fe4368616e67696e67206772656574696e672066726f6d202725732720746f2027257327a2646970667358221220d8b702fd211fe2829242f362fda35c636e2590000ac3fa2b44f0ac2fb1183cd564736f6c634300080400334465706c6f79696e67206120477265657465722077697468206772656574696e673a",
  "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae321714610057575b600080fd5b6100556004803603810190610050919061043d565b610075565b005b61005f61013c565b60405161006c91906104b7565b60405180910390f35b6101226040518060600160405280602381526020016106e3602391396000805461009e90610610565b80601f01602080910402602001604051908101604052809291908181526020018280546100ca90610610565b80156101175780601f106100ec57610100808354040283529160200191610117565b820191906000526020600020905b8154815290600101906020018083116100fa57829003601f168201915b50505050508361026a565b8060009080519060200190610138929190610332565b5050565b60606000805461014b90610610565b80601f016020809104026020016040519081016040528092919081815260200182805461017790610610565b80156101c45780601f10610199576101008083540402835291602001916101c4565b820191906000526020600020905b8154815290600101906020018083116101a757829003601f168201915b5050505050905090565b61026682826040516024016101e49291906104d9565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b5050565b61030483838360405160240161028293929190610510565b6040516020818303038152906040527f2ced7cef000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b505050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b82805461033e90610610565b90600052602060002090601f01602090048101928261036057600085556103a7565b82601f1061037957805160ff19168380011785556103a7565b828001600101855582156103a7579182015b828111156103a657825182559160200191906001019061038b565b5b5090506103b491906103b8565b5090565b5b808211156103d15760008160009055506001016103b9565b5090565b60006103e86103e384610581565b61055c565b90508281526020810184848401111561040057600080fd5b61040b8482856105ce565b509392505050565b600082601f83011261042457600080fd5b81356104348482602086016103d5565b91505092915050565b60006020828403121561044f57600080fd5b600082013567ffffffffffffffff81111561046957600080fd5b61047584828501610413565b91505092915050565b6000610489826105b2565b61049381856105bd565b93506104a38185602086016105dd565b6104ac816106d1565b840191505092915050565b600060208201905081810360008301526104d1818461047e565b905092915050565b600060408201905081810360008301526104f3818561047e565b90508181036020830152610507818461047e565b90509392505050565b6000606082019050818103600083015261052a818661047e565b9050818103602083015261053e818561047e565b90508181036040830152610552818461047e565b9050949350505050565b6000610566610577565b90506105728282610642565b919050565b6000604051905090565b600067ffffffffffffffff82111561059c5761059b6106a2565b5b6105a5826106d1565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b838110156105fb5780820151818401526020810190506105e0565b8381111561060a576000848401525b50505050565b6000600282049050600182168061062857607f821691505b6020821081141561063c5761063b610673565b5b50919050565b61064b826106d1565b810181811067ffffffffffffffff8211171561066a576106696106a2565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fe4368616e67696e67206772656574696e672066726f6d202725732720746f2027257327a2646970667358221220d8b702fd211fe2829242f362fda35c636e2590000ac3fa2b44f0ac2fb1183cd564736f6c63430008040033",
  "linkReferences": {},
  "deployedLinkReferences": {}
}

動作確認

これで一通りの機能が実装できたと思います。

もしご興味ある方はぜひ動作お試しください。

その際は、メインネットの資産が入っていないアカウントで、テストネットのETHをFaucet等から入手してお試しください。

  1. まず、Connectボタンをクリックして、MetaMaskとの接続を確立しておきましょう。
  2. 次に、Deployボタンをクリックして、コントラクトをデプロイしましょう。(一応、ここの処理の冒頭で、1と同様のMetaMaskとの接続要求処理を入れておいたので、1をやらずに2を実行しても動きます。)MetaMaskの画面の指示に応じて処理を行ってください。
  3. コントラクトをデプロイしたら、ブラウザの開発者ツールのconsoleにdeployTransaction is 1 confirmedというメッセージが流れるまで待ちましょう。このメッセージがでたら、トランザクションが承認されてブロックに組み込まれたことになります。(もちろん、Ethereumブロックチェーンの特徴として、承認されたブロックが巻き戻って、承認されたトランザクションが無かったことになってしまうリスクもありますが、今回の実装ではそういった点は考慮していません。)
  4. コントラクトをデプロイしたトランザクションが承認されたら、現状セットされている挨拶文を表示するGreetボタンをクリックしてみましょう。ボタンを押す毎に、現状セットされている挨拶文が追加表示されていくと思います。
  5. コントラクトは挨拶文を変更する関数の実行もサポートしているので、挨拶文をデプロイ直後のHello, world!から変更してみましょう。例えばこんにちは、世界!に変えてみると良いでしょう。インプットボックスに新しい挨拶文を入力してSet New Messageボタンをクリックすると、MetaMaskが起動するので画面の指示に応じて処理を行ってください。
  6. こちらのトランザクションも、トランザクションが承認されるまで待ちましょう。トランザクションが承認されたら、setGreetingTransaction is 1 confirmedというメッセージがブラウザの開発者ツールのconsoleに流れます。
  7. 挨拶文変更のトランザクションが承認されたら、挨拶文が変わっているか、Greetボタンをクリックして試してみましょう。意図通り挨拶文が変わっていたら正常に動作できています。

まとめ

ethersを用いてMetaMaskと連携してコントラクトデプロイやコントラクトの呼び出しを行う方法をAngularのサンプルWebアプリの作成を通して解説しました。

MetaMaskとethersがコントラクトをデプロイしたり、コントラクトを呼び出したりする部分の煩わしい処理をかなりシンプルに担ってくれていて、Webアプリ開発者としては、(コントラクトが既にあり、その仕様が明確であるという条件なら)かなりシンプルに実装できて、とても便利と改めて感じました。

Angularでブロックチェーンを扱う場合、SDKで使用されているcryptoモジュールを適切にバンドルするために環境構築等でちょっとした追加設定を行う必要がある場合が多く、今回のサンプルアプリ作成でもそういった手順が必要かなと思っていたのですが、結果的にそういった処理は不要でした。MetaMaskがそのあたりを一手に担ってくれているのか、ethersがそのあたりをうまく実装してくれているのかといった詳細はわかりませんが、便利でありがたいと感じました。

実際に開発してみることで、Ethereumのエコシステムで大きな存在感を持っているMetaMaskが開発者にとってどのような影響を持っているかを体感できたように思います。

他のブロックチェーンが同様のアプローチを進めていくのか?あるいはブラウザ拡張的な機能から一歩踏み込んだ新しいツールが生まれて時代を切り開いていけるのか?といった今後のブロックチェーン関連技術のトレンドにも注目しつつ、これからも、ブロックチェーン関連アプリ開発を色々と進めていきたいと思っています。

CauchyE は一緒に働いてくれる人を待っています!

ブロックチェーンやデータサイエンスに興味のあるエンジニアを積極的に採用中です!
以下のページから応募お待ちしております。
https://cauchye.com/company/recruit

Discussion