サイトのログインにWalletConnectを導入したい

まずはじめに「WalletConnect」とは、MetaMaskなどのウォレットアプリでの署名を利用して、サイトへのログインや取引承認ができる仕組みのこと。

これを利用することで、サイトごとにIDやパスワードを設定する必要がなくなり、ウォレットアプリのみで一元管理できるようになります。

イメージしやすく言うと、ウォレットのアドレスにサイト内のアカウント情報が紐づく感じ。

そんなWalletConnectを組み込むうえでの設定手順や注意点のまとめです。

WalletConnectはReactをメインに設計されている

WalletConnect公式にはJavaScriptなどでも設置できると表示されているが、実際には推奨されているReact以外での設置はほぼ不可能。(出来てもかなり面倒)

理由はインストールする各ライブラリのバージョンの一致が厳密すぎて、ほぼエラーになってしまうから。

Reactであれば自動でバージョンを揃えてインストールできるので、かなりスムーズに設置できる。

ということでこのページでもReactでの設置を解説していく。

全体の流れの簡単なまとめ

まずフォルダーを2つ作成します。

  • Reactでの部品作成用(①)
  • ログインを実装するサイト用(②)

Reactはページ内で使う部品であるため、ログインを実装するサイトのフォルダーとは別で作成します。

①で部品が完成したら、②に完成したコードをコピーして導入する感じです。

はじめはなんのこっちゃ?ですが、一通り触ってみれば理解できるかと思います。

HTMLで簡単なログインページとホームページを作成する

今回はWalletConnectでのログインが成功したか分かりやすくするために、ログインページとログインを通過した先で見れるホームページを作成します。

手間なので、CSSは考慮しないこととします。

まずはデスクトップに「WalletConnect-demo」フォルダーを作成し、その中に「templates」フォルダーを作成。

さらにその中にlogin.htmlを作成して以下のログインページを記述します。

<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>ログイン</title>
</head>

<body>
    <h1>ウォレットでログイン</h1>
   
    <div id="connect-wallet-btn" class="login-button"></div>
</body>

</html>

この「id=”connect-wallet-btnにReactで作成したWalletConnectをマウント表示させ、ウォレットアプリでの接続と署名を行いログインまで行う流れです。

また後ほどflaskで「アドレスと署名」の両方が取得できていない場合は、ログインできないようにも設定します。(この設定がないとアドレスをコピペするだけでログインできてしまうため必須)

そしたら同じく templates フォルダーに、ログイン成功時に表示させる「index.html」を作成します。

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>ホーム</title>
</head>
<body>

    <!-- サーバー側で差し込む想定(Jinja等) -->
    <p>ログイン済みです。</p>
    <p>ウォレットアドレス:<strong>{{ wallet_address }}</strong></p>


</body>
</html>

成功してページが表示されると「ウォレットアドレス:」にログインに使用したウォレットのアドレスが挿入されます。

そしたらこれを表示できるようにflaskでルートを定義します。

「WalletConnect-demo」フォルダー直下に「app.py」を作成し、以下のコードを記述。

from flask import Flask, render_template, session, redirect

app = Flask(__name__)
app.secret_key = "your_secret_key_here"

@app.route("/login")
def login_page():
    return render_template("login.html")

@app.route("/")
def home_page():
    if not session.get("wallet_address") or not session.get("wallet_signature"):
        return redirect("/login")

    return render_template("index.html", wallet_address=session["wallet_address"])


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, debug=True)

これでとりあえずターミナルに「python app.py」でポート8000(http://127.0.0.1:8000)にログインページが表示されるようになりました。

ホームページにアクセスしても、ウォレットアドレスと署名がセッションが保存されていないと/loginにリダイレクトされるようになっています。

このフォルダーでの作業は一旦終わりで、Reactで部品を作成する工程に入ります。

WalletConnect公式に登録してproject IDを取得する

まずWalletConnectを実装するには、WalletConnectの公式サイトから「project ID」を取得する必要があります。

メアドを登録して +create team でteamを作成し、+projectでプロジェクト名を「test」にしてcreate。

これでproject IDが生成されます。

公式サイトが以下のように表示されるので、「React」を選択。

そうするとReactで設置する場合の手順ページに遷移しますので、これを一通り読むのがまずはおすすめ。

英語なのでGoogle翻訳にかけてもいいです。設定で悩んだらまずはこれを見るようにしましょう。

そしたらReactの作成に入っていきます。

ReactとViteでの開発環境を整える

デスクトップに「React」フォルダーを作成し、まずは「React本体」と開発環境である「Vite」をインストールします。

ターミナルのコマンドから以下を実行。

npm install react react-dom
npm install -D vite @vitejs/plugin-react

// -D は開発環境だけにインストールする設定。最終ビルドには含まれない。

今回は、個人開発でちょっとWalletConnectに触れたいという前提なので、package.jsonにscriptsの設定はしません。

npm run dev ではなく、npx vite で開発サーバーを起動させることとします。

そしたらViteで表示させるためのすべての入り口になる「index.html」をプロジェクト直下(Reactフォルダー直下)に作成します。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Test</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

Viteは、プロジェクト直下の「index.html」を自動で読み込み、そこからReactのコードやjsを読み込む。
またReactのコードは「.jsx」というファイル拡張子になる。

次に、<script type=”module” src=”/src/main.jsx”></script> で指定した「src」フォルダーと「main.jsx」ファイルを作成します。

// main.jsx

import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(<App />);

このmain.jsxがHTMLの id=”root”にマウントする(置き換える)コードになります。

main.jsxは、ただの置き換え処理を担当するコードなので、同じsrcフォルダー内に「App.jsx」を作成し、WalletConnectのUI部分を記述していきます。

// App.jsx

import React from "react";

export default function App() {
  return <h1>Hello React!</h1>;
}

ここまで書いて、「npx vite」でサーバーを起動し、「http://localhost:5173」にアクセスして Hello React! が表示されたら、開発環境の構築は完了です。

必要なライブラリをインストールし、App.jsxにUIを作成していく

まずWalletConnectをReactで設置する場合の手順ページに記載されているコマンドをターミナルに打ち込み、必要なライブラリをインストールしていきます。

npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query

Reactであればこれだけで基盤が一通り揃いますが、JavaScriptだとひとつずつインストールしていくため、ここでバージョンの不一致が起きてエラーが連発します。

そしたらとりあえず公式の手順通りに沿ってUIを表示させてみましょう。

手順ページを読み進めていくと、「実装(Implementation)」のところに App.jsx に記載する以下のサンプルコードが出ています。

// App.jsx

import { createAppKit } from '@reown/appkit/react'

import { WagmiProvider } from 'wagmi'
import { arbitrum, mainnet } from '@reown/appkit/networks'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'

// 0. Setup queryClient
const queryClient = new QueryClient()

// 1. Get projectId from https://dashboard.reown.com
const projectId = 'YOUR_PROJECT_ID'

// 2. Create a metadata object - optional
const metadata = {
  name: 'AppKit',
  description: 'AppKit Example',
  url: 'https://example.com', // origin must match your domain & subdomain
  icons: ['https://avatars.githubusercontent.com/u/179229932']
}

// 3. Set the networks
const networks = [mainnet, arbitrum]

// 4. Create Wagmi Adapter
const wagmiAdapter = new WagmiAdapter({
  networks,
  projectId,
  ssr: true
})

// 5. Create modal
createAppKit({
  adapters: [wagmiAdapter],
  networks,
  projectId,
  metadata,
  features: {
    analytics: true // Optional - defaults to your Cloud configuration
  }
})

export function AppKitProvider({ children }) {
  return (
    <WagmiProvider config={wagmiAdapter.wagmiConfig}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </WagmiProvider>
  )
}

これをまるっとコピペした後、一番上に「import React from “react”;」を追加し、const projectId = ‘YOUR_PROJECT_ID’ の部分の事前に取得しておいたtestの project ID に置き換えます。

また const metadata = {} 内にある「url: ‘https://example.com’,」を「url: window.location.origin,」に変更し、今アクセスしているページを使うようにします。

.jsx ファイルでJSX構文(HTMLタグのような書き方)を使う場合、import React from “react”; の記述が必要になる場合があります。
Vite + React 17以上の新しい設定では省略可能ですが、互換性のために記述しておくことが推奨されます。

最後にある「export function AppKitProvider({ children }) 」は、関数コンポーネントの定義です。

これがあることでimport先の.jsxで、<AppKitProvider>で呼び出せるようになります。(Reactでは AppKitProvider() のように関数形式で呼び出すことはせず、タグ形式で記述します。)

これでモーダル内の設定は書けたので、これを呼び出すボタンをmain.jsxに記述します。

以下のように修正してください。

// main.jsx

import React from "react";
import { createRoot } from "react-dom/client";
import { AppKitProvider } from "./App.jsx";


createRoot(document.getElementById("root")).render(
  <AppKitProvider>
    <appkit-button />
  </AppKitProvider>
);

<appkit-button /> は、WalletConnectが用意してくれている公式のコンポーネントです。

そのため、手順通りのコマンドでライブラリをインストールしていれば自動的に使えます。

そしてこの<appkit-button />を包んでいる<AppKitProvider>がとても重要な役割を担っています。

<appkit-button />自体はイベントを発火させるただのボタンであり、ウォレットアドレスを取得したりモーダルを立ち上げたりする機能は持っていません。

そこで、<AppKitProvider> 内で WagmiProvider と QueryClientProvider を設定し、このラップ内全体に必要な接続情報や状態管理を提供することで、<appkit-button />が正しく動作するようになります。

この状態で npx vite でサーバーを立ち上げてアクセスしてみると、WalletConnectのボタンとモーダルが立ち上がります。

webページに表示されるボタン
ボタンを押すと立ち上がるモーダル

今の状態でもウォレットの起動から接続まではできており、ウォレットのアドレスも取得できています。

ただ署名が取得できていないので追記していきましょう。

<appkit-button /> は接続のUIだけなので、署名用のボタンとログインボタンを作成して追加する必要があります。

/src内に新たに「Wallet_login.jsx」を作成し、以下をコピペ。

import React, { useState } from "react";
import { useAccount, useSignMessage } from "wagmi";

function buildMessage(address, nonce) {
    return `Sign in as ${address}\nNonce: ${nonce}\nIssued At: ${new Date().toISOString()}`;
}

export function WalletLogin() {
    const { address, isConnected } = useAccount();
    const { signMessageAsync, isPending } = useSignMessage();
    const [signature, setSignature] = useState("");

    // 署名処理
    const handleSign = async () => {
        if (!isConnected || !address) return;

        let nonce = "static-nonce";
        // サーバーからnonceを取得できる場合
        if (window?.login?.getNonce) {
            try {
                const r = await window.login.getNonce();
                nonce = r?.nonce || nonce;
            } catch (err) {
                console.error("Nonce取得エラー:", err);
            }
        }

        const message = buildMessage(address, nonce);
        try {
            const sig = await signMessageAsync({ message });
            setSignature(sig);
        } catch (err) {
            console.error("署名エラー:", err);
        }
    };

    // ログイン処理(Flaskに送信)
    const handleLogin = async () => {
        try {
            const res = await fetch("/login-wallet", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                credentials: "include", // FlaskのセッションCookie送信
                body: JSON.stringify({ address, signature })
            });
            const result = await res.json();
            if (result.redirect) {
                window.location.href = result.redirect;

            } else {
                alert("ログイン失敗: " + (result.message || ""));
            }
        } catch (err) {
            console.error("通信エラー:", err);
            alert("通信エラーが発生しました");
        }
    };

    if (!isConnected) return null;

    return (
        <div style={{ marginTop: 12 }}>
            <button onClick={handleSign} disabled={isPending}>
                {isPending ? "署名中…" : "メッセージに署名する"}
            </button>
            {signature && (
                <div style={{ marginTop: 8, wordBreak: "break-all" }}>
                    <div><b>アドレス:</b> {address}</div>
                    <div><b>署名:</b> {signature}</div>
                    <button onClick={handleLogin}>ログイン</button>
                </div>
            )}
        </div>
    );
}

そしてmain.jsxにimport文とコンポーネントの指定を追記します。

import React from "react";
import { createRoot } from "react-dom/client";
import { AppKitProvider} from "./App.jsx";
import { WalletLogin } from "./Wallet_login.jsx"


createRoot(document.getElementById("root")).render(
  <AppKitProvider>
    <appkit-button />
    <WalletLogin />
  </AppKitProvider>
);

この状態で npx vite でサーバーを立ち上げてアクセスしてみると、「ウォレットを接続ボタン → WalletConnectが立ち上がる → メタマスクを選択でアプリ遷移 → 接続 → サイトに戻りメッセージに署名するボタンが表示 → 再びメタマスクに遷移して署名 → サイトに戻ってログインボタンが表示 → flaskに送信」まで完成しました!

WalletConnectで選択したネットワークとメタマスク側で表示されるネットワークが一致している必要があります。
これまでのコードで「import { arbitrum, mainnet } from ‘@reown/appkit/networks’」の部分を変更していなければ、自動的に「イーサリアム」が選択されます。
普段ポリゴンなど別のネットワークを使っている場合は、メタマスクがポリゴンで開いてしまいますので、ネットワークを変更するようにしてください。

ログインページのウォレットでログインボタンにReactをマウントさせる

main.jsx のマウント(置き換え)先が「createRoot(document.getElementById(“root”))」になっているため、ログインぺージの「ウォレットでログイン」ボタンのidに変更します。

// main.jsx

import React from "react";
import { createRoot } from "react-dom/client";
import { AppKitProvider} from "./App.jsx";
import { WalletLogin } from "./Wallet_login.jsx"


createRoot(document.getElementById("connect-wallet-btn")).render(
  <AppKitProvider>
    <appkit-button />
    <WalletLogin />
  </AppKitProvider>
);

次に、viteの(ビルド / 開発サーバー)設定ファイルである「vite.config.js」をReactフォルダー直下に作成。

以下の内容をコピペします。

// vite.config.js

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
    base: '/static/',
    plugins: [react()],
    build: {
        outDir: "dist",   // 出力フォルダ
        emptyOutDir: true,
        rollupOptions: {
            input: "./src/main.jsx"
        }
    }
});

そしたら「npx vite build」をターミナルで実行。

これでReactフォルダー直下に「dist/assets」フォルダーが作成され、この中にWalletConnectに使うjsがすべてビルドされました。

次にこのすべてのjsを「dist/assets」フォルダーごと、ログインページのある「WalletConnect-demo」フォルダーに以下のコードでコピペします。

Copy-Item -Path "C:\Users\ユーザー名\Desktop\React\dist\*" -Destination "C:\Users\ユーザー名\Desktop\WalletConnect-demo\static" -Recurse -Force

// ユーザー名は自分のPCのものに置き換え

// -Recurse distフォルダーの中を階層ごと全部含める。
// -Force 確認なしで上書きする。

コピペ先の「static/assets」フォルダーの中にmainから始まる「main-〇〇〇〇.js」があるので、そのパスを指定してログインページHTMLの</body>直前で読み込ませます。

<script type="module" src="{{ url_for('static', filename='assets/main-〇〇〇〇.js') }}"></script>

ターミナルで「python app.py」でflaskアプリを起動。

ログインページにWalletConnectのボタンが表示される。

うまくできていれば「アドレス → 署名 → ログインボタン」が表示される。

ログインボタンでの送信をflaskで処理

最後にログインボタンで送信されてきたアドレスと署名をsessionに保存して、”/” にリダイレクトさせるエンドポイントを作成します。

app.pyに以下を追記。

@app.route("/login-wallet", methods=["POST"])
def login_wallet():
    data = request.get_json()
    address = data.get("address")
    signature = data.get("signature")

    session["wallet_address"] = address
    session["wallet_signature"] = signature
   return jsonify({"redirect": "/"})

これでログインボタンを押すとindex.htmlにリダイレクトされるようになりました。

セキュリティを強化する