アプリケーション開発ポータルサイト
ServerNote.NET
Amazon.co.jpでPC関連商品タイムセール開催中!
カテゴリー【JavaScript仮想通貨Node.js
ブラウザのJavaScriptのみでMetaMaskと連携しトランザクションを実行する
POSTED BY
2023-05-31

Ethereumネットワークの標準財布ソフトであるMetaMaskと連携するメモ。
サーバー上のNode.jsを使わずとも、ブラウザのHTML + Javascriptのみでほとんどの操作が可能。

公式のドキュメントは以下。

https://docs.metamask.io/guide/

MetaMaskのインストールはOpenSeaで画像をNFT資産として販売するにはの、
【4、仮想通貨ウオレットプラグインMETAMASKをChrome拡張機能でインストール】など参照してください。

完成されたサンプルはこちら

・MetaMaskのインストール状況を調べ、OKなら接続ボタンでネットワーク・アカウント・ETH残高を取得し表示。
・任意の送金先アドレス・送金額・任意データをフォームで受け取り、MetaMaskを通じて送金トランザクションを発行する。

以下、順番に説明します。最後にHTMlファイル全文を貼り付けています。

1、MetaMaskのインストール有無を確認する

var valid_mm = false;
if (typeof window.ethereum !== 'undefined') {
  valid_mm = true;
  $("#valid_mm_log").html("MetaMaskが認識できました。");
}
else {
  valid_mm = false;
  $("#valid_mm_log").html("MetaMaskがインストールされていません。");
}

window.ethereumという型自体が存在するかどうかで判別する。

2、MetaMaskと接続し選択ネットワーク・アカウント・ETH残高を取得する

const wei2eth = 0.000000000000000001;
const eth2wei = 1000000000000000000;

var account_mm = "";
var network_id = 0;
var network_name = "";
var balance = 0;

async function connect_metamask() {
  valid_metamask();

  if(!valid_mm) {
    return;
  }

  $("#connect_mm_log").html("MetaMaskと接続中...");

  try {
    var accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
    if (accounts.length > 0) {

      //https://chainlist.org/
      var network_names = {
        1: "イーサリアムメインネット",
        3: "Ropstenテストネットワーク",
        4: "Rinkebyテストネットワーク",
        42: "Kovanテストネットワーク",
        61: "イーサリアムClassicメインネット",
      };

      var network_id_raw = await window.ethereum.request({ method: 'eth_chainId' });
      network_id = parseInt(network_id_raw);
      network_name = "不明";

      if (network_id in network_names) {
        network_name = network_names[network_id];
      }

      var msg = "MetaMaskと接続できました。<br>接続しているネットワーク:" + network_name;

      account_mm = accounts[0];
      msg += "<br>アカウントのアドレス:" + account_mm;

      var amount = await window.ethereum.request({
        method: "eth_getBalance",
        params: [
          account_mm,
          "latest",
        ],
      });
      // wei -> ether
      balance = parseInt(amount) * wei2eth;

      msg += "<br>イーサリアム残高:" + balance + "ETH";

      $("#connect_mm_log").html(msg);

    }
  } catch (err) {
    if (err.code === 4001) {
      // EIP-1193 userRejectedRequest error
      $("#connect_mm_log").html("接続が拒否されました。<br>" + err.message);
    } else {
      console.error(err);
      $("#connect_mm_log").html("接続エラーが発生しました。<br>" + err.message);
    }
  }
}

window.ethereum.requestでeth_requestAccountsメソッドを呼ぶとMetaMaskへの接続要求がなされ、MetaMaskのポップアップが起動し、このサイトに接続しても良いかどうかの認証ダイアログが出る。「接続しない」にすると、catchで例外errに認証拒否の旨が返るので、エラー出力する。

接続が許可されると、戻り値にアカウントアドレスの文字列配列が返る。選択中のアカウント文字列は、配列の0番目(先頭)で参照する。

接続許可後はMetaMaskの各種APIが呼べるようになる。まずeth_chainIdメソッドで、選択中のネットワークIDを取得して、対応するネットワーク名に置き換える。ネットワークIDと名称の一覧は以下。

https://chainlist.org/

eth_getBalanceメソッドで、アカウントのイーサリアム残高が取得できる。単位はweiで返るので、

const wei2eth = 0.000000000000000001;

を乗じることで、ETH単位に変換できる。

3、選択ネットワークやアカウントの切り替えに反応する

MetaMaskのウィンドウにて、ネットワークやアカウントは随時切り替えられる。それに反応するためのイベントハンドラを設置できる。

window.ethereum.on("accountsChanged", (accountNo) => {
  connect_metamask();
});

window.ethereum.on("chainChanged", (accountNo) => {
  connect_metamask();
});

イベントが来たら、随時connect_metamaskを呼んで、情報を更新してやればよい。

4、任意のアドレスに任意のデータとETHを送金する

async function send_transaction() {

  trimingForm("to_address_txt");
  trimingForm("to_amounts_txt");
  trimingForm("to_data_txt");
  var to_data_txt = $("#to_data_txt").val();
  if (to_data_txt.length > 0) {
    to_data_txt = to_data_txt.replace(/\r\n/g, "\n");
    to_data_txt = to_data_txt.replace(/\n/g, "");
  }
  else {
    to_data_txt = "";
  }

  if($("#to_address_txt").val().length <= 0 ||
    $("#to_amounts_txt").val().length <= 0) {
    $("#send_amounts_log").html("正しい値を入力してください。");
    return;
  }

  connect_metamask();

  if(network_id == 1 || network_id == 61) {
    $("#send_amounts_log").html("本番ネットワークでの送金はできません。");
    return;
  }

  if(parseFloat($("#to_amounts_txt").val()) > balance) {
    $("#send_amounts_log").html("送金額相当のETHを保持していないようです。");
    return;
  }

  if($("#to_address_txt").val() == window.ethereum.selectedAddress) {
    $("#send_amounts_log").html("自分自身への送金はできません。");
    return;
  }

  $("#send_amounts_log").html("送金トランザクション実行中...");

  try {

    var send_wei = parseFloat($("#to_amounts_txt").val()) * eth2wei;
    var send_wei_str = "0x" + send_wei.toString(16);
    var send_params = {
      nonce: '0x00',
      gasPrice: '', 
      gas: '',
      to: $("#to_address_txt").val(),
      from: window.ethereum.selectedAddress,
      value: send_wei_str,
      data: to_data_txt,
      chainId: '',
    };

    var transaction_id = await window.ethereum.request({
      method: 'eth_sendTransaction',
      params: [send_params],
    });

    $("#send_amounts_log").html("トランザクション完了:ID " + transaction_id);
  }

  catch (err) {
    console.error(err);
    $("#send_amounts_log").html("トランザクションエラーが発生しました。<br>" + err.message);
  }
}

まずはフォームパラメータフィールドから空白や改行を除く正規化処理をします。

「送金先アカウントアドレス」には人であるEOAだけでなく、コントラクトのアドレスも指定できます。
その場合は「任意データフィールド」に呼ぶコントラクトの関数とパラメータをABIエンコードした16進文字列を指定します。EOAにお金だけ送る場合通常任意データフィールドはカラにします。

connect_metamaskを呼んで、情報を最新にしておきます。

間違ってメインネットで送金してしまうことの無いよう、当サンプルではnetwork_idをチェックし1か61なら実行しないようにしている。

入力された送金額と、現在のアカウントの残高を比較し、少なければ実行しない。

送金先のアカウントアドレスと現在のアカウントアドレスが同じならば実行しない。

ETHとして入力を受けた送金額は、Weiに変換するため、

const eth2wei = 1000000000000000000;

を乗じることでWeiに変換し、0xを付与した16進文字列にするためtoString(16)で変換。送金額はパラメータのvalueフィールドで指定する。

fromに現在選択中のアドレス、toに送金先アドレスを指定してeth_sendTransactionを実行する。gas関係のフィールドはカラにしておけば、MetaMaskのダイアログで確認したとおりの値で埋めてくれる。

MetaMaskの認証ダイアログにて、送金額、発生予想ガス代を確認して、問題なければ「実行」してトランザクション実行。

成功すると、戻り値にトランザクションIDの文字列が返る。このIDをetherscanに貼り付ければ詳細を見ることができる。

送金元から指定分のETHとガス代が引かれ、送金先にETHが加算されているのが確認できるはずである。

なお、NFTなどのスマートコントラクトを送金先に設定し、dataフィールドで呼びたいコントラクト関数の16進文字列を指定すれば、現存するスマートコントラクトのコードですらブラウザ上で実行できる。

HTML+Javascript全文は以下になります。

HTMLmetamask.htmlGitHub Source
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>MetaMaskテストページ</title>

<style type="text/css">

body {
  word-break: break-all;
  word-wrap: break-word;
  margin-bottom: 50px;
}

textarea {
  line-height: 1.2em;
}

</style>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </head>

<body class="container-fluid m-3">

<div class="row mb-2">
<div class="col-12">
<span style="font-size: 2rem;">MetaMaskテストページ</span><span class="mx-2">※REVISION 0.0.0※</span>
</div>
</div>

<div class="row mb-3">
  <div id="valid_mm_log" class="col-12"></div>
</div>

<div class="row mb-3">
  <div class="col-12">
<button id="connect_mm_btn" type="button" class="btn-danger p-2 rounded" style="width:250px;">MetaMaskと接続する</button>
  </div>
</div>

<div class="row mb-3">
  <div id="connect_mm_log" class="col-12"></div>
</div>

<div class="row mb-3">
  <div class="col-12">
送金先アカウントアドレス(0x~):<br>
<input id="to_address_txt" type="text" style="width:400px;"><br>
送金額:<input id="to_amounts_txt" type="text" style="width:100px;" value="0.01">ETH<br>
任意データフィールド(0x~):<br>
<textarea id="to_data_txt" rows="6" style="width: 400px;"></textarea>
  </div>
</div>

<div class="row mb-3">
  <div class="col-12">
<button id="send_amounts_btn" type="button" class="btn-primary p-2 rounded" style="width:250px;">上記内容で送金する</button><b style="color:#ff0000;">※自己責任注意※</b>
  </div>
</div>

<div class="row mb-3">
  <div id="send_amounts_log" class="col-12"></div>
</div>

<div class="row mb-3">
  <div class="col-12">
※本サンプル使用によるいかなる損害が発生しても当サイトおよび作成者は一切責任を負いませんのでご注意ください※
  </div>
</div>

<script>//<![CDATA[

  const wei2eth = 0.000000000000000001;
  const eth2wei = 1000000000000000000;

  var valid_mm = false;
  var account_mm = "";
  var network_id = 0;
  var network_name = "";
  var balance = 0;

  function escape_html (string) {
    if(typeof string !== 'string') {
      return string;
    }
    return string.replace(/[&'`"<>]/g, function(match) {
     return {
        '&': '&amp;',
        "'": '&#x27;',
        '`': '&#x60;',
        '"': '&quot;',
        '<': '&lt;',
        '>': '&gt;',
      }[match]
    });
  }

  function trimingForm(parts) {
    if( $('#' + parts).length ) {
      var pval = $('#' + parts).val();
      if( pval.length > 0 ){
        pval = pval.trim();
        $('#' + parts).val(pval.trim());
      }
    }
  }

  function valid_metamask() {
    if (typeof window.ethereum !== 'undefined') {
      valid_mm = true;
      $("#valid_mm_log").html("MetaMaskが認識できました。");
    }
    else {
      valid_mm = false;
      $("#valid_mm_log").html("MetaMaskがインストールされていません。");
    }
  }

  async function connect_metamask() {
    valid_metamask();

    if(!valid_mm) {
      return;
    }

    $("#connect_mm_log").html("MetaMaskと接続中...");

    try {
      var accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      if (accounts.length > 0) {

        //https://chainlist.org/
        var network_names = {
          1: "イーサリアムメインネット",
          3: "Ropstenテストネットワーク",
          4: "Rinkebyテストネットワーク",
          42: "Kovanテストネットワーク",
          61: "イーサリアムClassicメインネット",
        };

        var network_id_raw = await window.ethereum.request({ method: 'eth_chainId' });
        network_id = parseInt(network_id_raw);
        network_name = "不明";

        if (network_id in network_names) {
          network_name = network_names[network_id];
        }

        var msg = "MetaMaskと接続できました。<br>接続しているネットワーク:" + network_name;

        account_mm = accounts[0];
        msg += "<br>アカウントのアドレス:" + account_mm;

        var amount = await window.ethereum.request({
          method: "eth_getBalance",
          params: [
            account_mm,
            "latest",
          ],
        });
        // wei -> ether
        balance = parseInt(amount) * wei2eth;

        msg += "<br>イーサリアム残高:" + balance + "ETH";

        $("#connect_mm_log").html(msg);

      }
    } catch (err) {
      if (err.code === 4001) {
        // EIP-1193 userRejectedRequest error
        $("#connect_mm_log").html("接続が拒否されました。<br>" + err.message);
      } else {
        console.error(err);
        $("#connect_mm_log").html("接続エラーが発生しました。<br>" + err.message);
      }
    }

  }

  $(window).on('pageshow',function(){
    valid_metamask();
  });

  $("#connect_mm_btn").on("click", function() {
    connect_metamask();
  });

  window.ethereum.on("accountsChanged", (accountNo) => {
    connect_metamask();
  });

  window.ethereum.on("chainChanged", (accountNo) => {
    connect_metamask();
  });

  $("#send_amounts_btn").on("click", async function() {

    trimingForm("to_address_txt");
    trimingForm("to_amounts_txt");
    trimingForm("to_data_txt");
    var to_data_txt = $("#to_data_txt").val();
    if (to_data_txt.length > 0) {
      to_data_txt = to_data_txt.replace(/\r\n/g, "\n");
      to_data_txt = to_data_txt.replace(/\n/g, "");
    }
    else {
      to_data_txt = "";
    }

    if($("#to_address_txt").val().length <= 0 ||
      $("#to_amounts_txt").val().length <= 0) {
      $("#send_amounts_log").html("正しい値を入力してください。");
      return;
    }

    connect_metamask();

    if(network_id == 1 || network_id == 61) {
      $("#send_amounts_log").html("本番ネットワークでの送金はできません。");
      return;
    }

    if(parseFloat($("#to_amounts_txt").val()) > balance) {
      $("#send_amounts_log").html("送金額相当のETHを保持していないようです。");
      return;
    }

    if($("#to_address_txt").val() == window.ethereum.selectedAddress) {
      $("#send_amounts_log").html("自分自身への送金はできません。");
      return;
    }

    $("#send_amounts_log").html("送金トランザクション実行中...");

    try {

      var send_wei = parseFloat($("#to_amounts_txt").val()) * eth2wei;
      var send_wei_str = "0x" + send_wei.toString(16);
      var send_params = {
        nonce: '0x00', // ignored by MetaMask
        gasPrice: '', // customizable by user during MetaMask confirmation.
        gas: '', // customizable by user during MetaMask confirmation.
        to: $("#to_address_txt").val(), // Required except during contract publications.
        from: window.ethereum.selectedAddress, // must match user's active address.
        value: send_wei_str, // Only required to send ether to the recipient from the initiating external account.
        data: to_data_txt, // Optional, but used for defining smart contract creation and interaction.
        chainId: '', // Used to prevent transaction reuse across blockchains. Auto-filled by MetaMask.
      };

      var transaction_id = await window.ethereum.request({
        method: 'eth_sendTransaction',
        params: [send_params],
      });

      $("#send_amounts_log").html("トランザクション完了:ID " + transaction_id);
    }

    catch (err) {
      console.error(err);
      $("#send_amounts_log").html("トランザクションエラーが発生しました。<br>" + err.message);
    }

  });

//]]></script>


</body>
</html>

※本記事は当サイト管理人の個人的な備忘録です。本記事の参照又は付随ソースコード利用後にいかなる損害が発生しても当サイト及び管理人は一切責任を負いません。
※本記事内容の無断転載を禁じます。
【WEBMASTER/管理人】
自営業プログラマーです。お仕事ください!
ご連絡は以下アドレスまでお願いします★

☆ServerNote.NETショッピング↓
ShoppingNote / Amazon.co.jp
☆お仲間ブログ↓
一人社長の不動産業務日誌
【キーワード検索】