Web3技術の勉強の一環として、ウォレットを使ったログインを実装してみました。
MetaMaskのインストールはこちらから。
ビルダとしてwebpackを使用し、UIライブラリとしてsvelteを使用しました。ランナー/パッケージマネージャはnpmです。svelte以外は定番構成だと思います。(disclaimer: 僕はフロントエンドエンジニアではないので妙な表現になっているかも)
Svelteとは、軽量なUIライブラリの一種で、カテゴリとしてはReactやVue.jsの仲間ですが、virtual DOMを使わないといった特徴があります。ゴリゴリ動かないアプリケーションであれば、これで良いと個人的には思っています。
CSSにはMarxという素朴なCSSを使いました。
言語はvanillaなJSを使いました。大規模になってきたらTypeScriptを使うとよさそう。
Ethereumを扱うweb3.jsと、そのsvelteバインディングであるsvelte-web3を使ってMetaMaskログインを実装しました。
動作
ログインボタンをクリックすると、MetaMaskがポップアップします。
ログインに使いたいアドレスを選ぶと、「ログインによってこのサイトはアドレスを見られるようになりますよ」といった許認可のページへと移り、これを許可するとログインが完了します。
ログインが完了すると、サイト側のJSがユーザのウォレットアドレスを読み取って表示します。
基礎知識
各用語の簡単な説明です。
- いわゆるWeb3の主役となるブロックチェーンがEthereum
- 拡張機能やモバイルアプリとして使えるEthereumウォレットがMetaMask*1
- Ethereumネットワークと通信し、ブロックチェーンとの通信を行うのはMetaMaskにやってもらう
- MetaMaskとJSとの橋渡しを
web3.js
が行う web3.js
単体ではファンダメンタルな操作しか行えないので、svelte-web3
がある程度これらをラップしてsvelteで使いやすくしている
設計
設計と呼べるほどのものはないのですが、主となるコンポーネントであるApp
に、MetaMaskLogin
とLoginStatus
というコンポーネントが入っています。
App
にはページの文言などが書かれています(一部略):
<script> import MetaMaskLogin from "./MetaMaskLogin.html"; import LoginStatus from "./LoginStatus.html"; import * as Marx from "marx-css"; import { web3 } from 'svelte-web3' let walletAddr = undefined; const goLoginStatus = (addr) => { walletAddr = addr }; </script> <svelte:head><!-- web3.js is not compatible with webpack/svelte... --> <script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script> </svelte:head> <main> <center><h1>Welcome to #web3 特設ページ</h1></center> <section> Web3.jsのテストページです。 <ul> <li>このページは、MetaMaskを使ってログインすることができます</li> <li>このページは、ログインによってウォレットアドレス(公開情報)を読み取ることができます</li> <li>ログインによって勝手にトランザクションを開始することはありません</li> </ul> </section> <section> <LoginStatus walletAddr={walletAddr} /> <MetaMaskLogin goLoginCB={goLoginStatus} /> </section> </main>
MetaMaskLogin
は実際にログインを行い、ログイン後には渡されたコールバックを呼び出します。コンポーネントを跨いだ状態変更の方法がわからなかったのでこういう形式で書いていますが、Svelte識者の方なら良い方法を知ってそう。
<script> import { connected, web3, selectedAccount, chainId, chainData } from 'svelte-web3' import { defaultEvmStores } from 'svelte-web3' export let goLoginCB; const loginTrigger = async () => { console.log("clicked login button"); await defaultEvmStores.setProvider(); $web3.eth.getAccounts() .then((acs) => { console.log(acs); goLoginCB(acs[0].toLowerCase()) }) } </script> {#if !$connected} <p>MetaMask: 未接続</p> <button on:click={loginTrigger}>Login with MetaMask</button> {:else} <p>MetaMask: chain {$chainId} に接続</p> {/if}
最後に、ログイン状態とログイン時のウォレットアドレスを表示するLoginStatus
です:
<script> import { connected, web3, selectedAccount, chainId, chainData } from 'svelte-web3' import { defaultEvmStores } from 'svelte-web3' export let walletAddr = undefined; </script> <div> <p>ログインステータス: {#if $connected}<span><address><code>{walletAddr}</code></address></span>{:else}<span>ログインしていません</span>{/if}</p> </div>
動作は至って簡単で、ログインボタンを押すとMetaMaskに「ログインしてええか?」と聞かれるので、「はい」と答えるとページ側はユーザのウォレットアドレスを知ることができる、という仕組みです。他にもログインすると様々な操作が可能になるので、ユーザになにかを署名してもらうといった操作も可能です。これはブロックチェーンの外で行える行為で、内部的にはウォレットの秘密鍵で署名しているだけです。ブロックチェーンとの連携が必須のように思われているWeb3ですが、別にブロックチェーンを使わなくてもMetaMaskを便利なログイン機構として使うことができます。
MetaMaskを使ったログインの利点は以下の通りです:
- 自分たちでログイン基盤を維持しなくてもよい
- APIを使うための費用がかからない
- そのかわり、ブロックチェーンに対する操作にはユーザ側に費用が発生する
- ブロックチェーンを使わなければ無視できる
- 署名という形でなんらかの検証可能な意思表示ができる
- ログインの永続化(cookie的なトークンに署名してもらう)
- サーバサイドへのリクエストに署名してもらう(投稿などで使えそう)
応用
ログインの永続化
今回はページをロードするたびにログイン操作が必要ですが(二度目以降はMetaMaskが覚えてくれるのでクリックするだけでよい)、ログインを覚えてもらうにはLocalStorageにウォレットIDを仮保存しておくと良さそうです。
よくあるのは、「このウォレットを記憶する」ボタンが用意されていて、クリックするとnonceを含んだJSONに対して署名(署名自体に費用は発生しません)を要求され、署名するとサーバ(もしくはクライアント)がこれを記憶しておくという方式です。jwt/joseみたいな感じですね。
NFT
NFTの取引プラットフォーム*2であるopensea.ioはAPIを提供しており、Ethereum上に
MetaMaskでウォレットIDを入手した後は、このAPIに尋ねて特定のNFTを所持しているかを確認することができます:
let openseaInfo; let walletAddr = undefined; // ページロード時にあらかじめ問い合わせておく onMount(async () => { const contractAddr = '0x2953399124F0cBB46d2CbACD8A89cF0599974963'; const itemId = '90321873922772702706889828053003954411909833063523571139972248233977405505636'; // opensea API v1 is not compatible with MATIC. wait for v2. fetch(`https://api.opensea.io/api/v1/asset/${contractAddr}/${itemId}/owners?format=json`) .then(res => res.json()) .then(j => { openseaInfo = j }) .catch(err => console.error(err)); }); // TODO:paging // ログイン後にこれを呼ぶ const goPossession = () => { console.log(`walletAddr : ${walletAddr}`); const ownerAddressList = openseaInfo['owners'].map(o => o['owner']['address'].toLowerCase()); console.log(ownerAddressList); if (ownerAddressList.includes(walletAddr)) { console.log('You have'); } else { console.log("You don't have"); } };
後はうまく出し分けを実装すれば、NFT所有者のみが閲覧できるちょっとしたコンテンツを実装できそうです。厳密なアクセス管理が必要であれば、さらにユーザに署名を求めて、署名したデータをサーバ側に通信して確認してもらうといった行為ができそうです。
類似品
Web3でのログインを行うためにはweb3authといった高級なプラットフォームもありますが、個人アプリでちょっとしたログインを提供したいというだけであれば手書きでも十分そうです。
感想
何かと怪しいイメージを持たれがちなWeb3関連技術ですが、怪しいのは仮想通貨で大儲けとかNFTで大儲けとか言ってる連中であって、技術的には面白い要素がいっぱいあります。今回はその中でも白眉だと個人的に考えるウォレットログインの実装を紹介しました。