0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

メタマスクに接続, アカウント切り替え, ネットワーク切り替え【Next.js】

Posted at

はじめに

本記事では、

  • メタマスクに接続する方法、
  • メタマスクのアカウント切り替えに対応する方法
  • ネットワーク切り替えに対応する方法

を書いていきます。

利用技術

  • Next.js
  • Recoil
  • TypeScript

リポジトリ

Next.js + Recoil 環境構築

Next.js + Recoilで環境構築します。
詳しい説明は省くので各自お願いします。

全く分からないという人は下記折り畳み部分を参考にしてみてください!!(解説なし)

Next.jsの環境構築

今回僕は、昔作ったTemplate Repositoryを利用してNext.jsの環境構築します。
(サンプル用だから適当です。)
https://github.com/tokio-dev/nextjs-template

  1. tokio-dev/nextjs-templateを開く
  2. 「Use this template」をクリックする
  3. Repository name等を入力してリポジトリを作成する
  4. 作成したリポジトリをcloneする
  5. yarn を実行する (ライブラリのインストール)
  6. yarn devを実行する (devモードで実行)
  7. ブラウザでlocalhost:3000を開く
  8. 開ければ環境構築完了
Recoilを使う準備
  1. yarn add recoilを実行する (Recoilのインストール)
  2. pages/_app.tsxを編集する (RecoilRootで囲う)

pages/_app.tsxを編集する (RecoilRootで囲う)

.tsx
{getLayout(<Component {...pageProps} />)}

.tsx
<RecoilRoot>{getLayout(<Component {...pageProps} />)}</RecoilRoot>

ディレクトリ構成等も同じにする必要はありません!
今回はサンプルなので、分かりやすくする為に、基本的に src/utils/walle の中に作っていきます。

windowの型を拡張

window.etherumを使ってMetamaskと接続するので、初めにwindowの型を拡張しておきます。

src/types/window.d.ts
interface Window {
  ethereum: undefined | Record<string, any>;
}

いい方法が分かってないので分かる方いましたらコメントください。🙇‍♂️

{ ethereum: any }にしてない理由

こうすることで、!window.ethereumの分岐の入れ忘れが防げます。
safari等、window.etherumが使えない場合もあり、分岐し忘れてエラーになることを防いでいます。
window.ethereum.onなどで警告を出してくれます。

アカウント(アドレス)を保存

global state(atom)の作成

src/utils/wallet/account.ts
import { atom, useRecoilValue, useSetRecoilState } from "recoil";

type Account =
  | { address: string | undefined; errorMessage: undefined }
  | { address: undefined; errorMessage: string | undefined };

const currentAccountState = atom<Account>({
  key: "currentAccount",
  default: { address: undefined, errorMessage: undefined },
});

global stateを扱うHooksの作成

src/utils/wallet/account.ts
import { atom, useRecoilValue, useSetRecoilState } from "recoil";

type Account =
  | { address: string | undefined; errorMessage: undefined }
  | { address: undefined; errorMessage: string | undefined };

const currentAccountState = atom<Account>({
  key: "currentAccount",
  default: { address: undefined, errorMessage: undefined },
});

//↓ 追加
export const useAccount = () => useRecoilValue(currentAccountState);
export const useSetAddress = () => {
  const setState = useSetRecoilState(currentAccountState);
  return (address: string) => setState({ address, errorMessage: undefined });
};
export const useSetAddressError = () => {
  const setState = useSetRecoilState(currentAccountState);
  return (errorMessage: string) => setState({ address: undefined, errorMessage });
};

※ 利用する側での扱い方を限定するためにcurrentAccountStateを直接exportせずに、このstateを扱うHooksを作成してexportしています。accout情報の取得(useAccount), addressの設定(useSetAddress), errorの設定(useSetAddressError)の3つの処理が出来ます。

walletと接続する関数の作成

src/utils/wallet/useWallet.ts
import { useCallback } from "react";

import { useAccount, useSetAddress, useSetAddressError } from "./account";

export const useWallet = () => {
  const setAddress = useSetAddress();
  const setError = useSetAddressError();
  const { address, errorMessage } = useAccount();

  const handleConnect = useCallback(async () => {
    try {
      if (!window.ethereum || !window.ethereum.isMetaMask) {
        setError("Metamaskをインストールしてください");
        return;
      }

      if (window.ethereum.chainId !== "0x1") {
        setError("イーサリアムメインネットでのみ利用できます。ネットワークを切り替えてください。");
        return;
      }

      const accounts = await window.ethereum.request({
        method: "eth_requestAccounts",
      });

      if (accounts.length !== 0) {
        setAddress(accounts[0]);
      } else {
        setError("認証済みのアカウントが見つかりませんでした");
      }
    } catch (error) {
      setError("Metamaskの接続に失敗しました");
    }
  }, [setAddress, setError]);

  return { handleConnect, address, error: errorMessage };
};

※ サンプルリポジトリでは、useWalletのみをexportするようにしています。

サンプルリポジトリのコードでは、useWalletのみがsrc/utils/wallet以外から利用できるようにしています。

exportしているものに以下を付与し、

.ts
/**
 * @package
 */

src/utils/wallet/index.tsを作成することで実現できます。

src/utils/wallet/index.ts
export { useWallet } from "src/utils/wallet/useWallet";

uhyoさんのこれです。
これはメインの内容とは関係ないので無視しても構いません。

※ handleConnect, address, errorをuseWalletに纏めなくても構いません。

walletに接続してアカウントを保存

関数を作ったので、実際に利用してみます。

src/pages/index/index.tsx
import type { FC } from "react";
import { useWallet } from "src/utils/wallet";

export const Index: FC = () => {
  const { address, error, handleConnect } = useWallet();

  return (
    <div>
      <button onClick={handleConnect}>Connect Wallet</button>
      {address ? <p>{address}</p> : null}
      {error ? <p>{error}</p> : null}
    </div>
  );
};
何もしてない時 接続に成功した時 ネットワークが違う時
スクリーンショット 2022-11-12 23.45.26.png スクリーンショット 2022-11-12 23.46.24.png スクリーンショット 2022-11-12 23.45.34.png

無事に接続中のアカウント名を表示することができました。
このままではリロードした時や開き直した時に、接続中のアカウントが消えてしまうので、消えないようにしていきます。

アカウントの初期値設定

src/utils/wallet/InitializeAccount.tsxに、初期化用のuseEffectを実行するのみのコンポーネントを作っていきます。
Reactであれば、atomにeffectsを追加することで実現できると思いますが、Next.jsではサーバー側でレンダリングされてしまいwindowにアクセスできないので、初期化専用のコンポーネントを作っていきます。
クライアント側で以下に記載する処理の実行さえできれば何を使っても構いません。

より良い方法があったらコメントして欲しいです🙇‍♂️

src/utils/wallet/InitializeAccount.tsx
import { useEffect } from "react";

import { useSetAddress, useSetAddressError } from "./account";

export const InitializeAccount = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const setAddress = useSetAddress();
  const setError = useSetAddressError();

  useEffect(() => {
    if (!window.ethereum || !window.ethereum.isMetaMask) {
      setError("Metamaskをインストールしてください");
      return;
    }

    window.ethereum
      .request({ method: "eth_accounts" })
      .then((accounts: any) => {
        if (window.ethereum?.chainId !== "0x1") {
          setError("イーサリアムメインネットでのみ利用可能です");
          return;
        }
        if (accounts.length !== 0) {
          setAddress(accounts[0]);
        }
      });
  }, [setAddress, setError]);
  return <>{children}</>;
};

InitializeAccountをRecoilRootの下で囲みます。

pages/_app.tsx
//..省略
<RecoilRoot>
  <InitializeAccount>
    {getLayout(<Component {...pageProps} />)}
  </InitializeAccount>
</RecoilRoot>
//...省略

開いたタイミングで、handleConnectの処理と同じようなことをして、その値を使って初期化しています。
これでリロードしてもアカウント名が表示されたままになりました。

アカウントの切り替え

次にMetamask上でアカウントを切り替えた時に、アカウントが切り替わるようにします。
現在はMetamask上でアカウントを切り替えた後に、Connect Walletボタンをクリックすることで切り替わります。
これをMetamask上での切り替えと同時に自動で切り替わるようにします。

追加するコードは以下です。

.tsx
// アカウント切り替え時の処理
const accountChanged = async (newAccount: string) => {
  setAddress(newAccount[0]);
};
window.ethereum.on("accountsChanged", accountChanged);

src/utils/wallet/InitializeAccount.tsxのuseEffect内に追加します。

src/utils/wallet/InitializeAccount.tsx
//...省略
  useEffect(() => {
    if (!window.ethereum || !window.ethereum.isMetaMask) {
      setError("Metamaskをインストールしてください");
      return;
    }

    // アカウント切り替え時の処理
    const accountChanged = async (newAccount: string) => {
      setAddress(newAccount[0]);
    };
    window.ethereum.on("accountsChanged", accountChanged);

    window.ethereum
      .request({ method: "eth_accounts" })
      .then((accounts: any) => {
        if (window.ethereum?.chainId !== "0x1") {
          setError("イーサリアムメインネットでのみ利用可能です");
          return;
        }
        if (accounts.length !== 0) {
          setAddress(accounts[0]);
        }
      });
  }, [setAddress, setError]);
//...省略

window.ethereum.on("accountsChanged", accountChanged); でアカウントを切り替えた時に、切り替えたアカウントをstateにセットするようにします。

これで、アカウント切り替えに対応できるようになりました。

ネットワークの切り替え

次にネットワークの切り替えに対応していきます。
Metamask上で、メイン以外のネットワークに切り替えた場合に、そのタイミングでエラーを表示するようにします。

追加するコードは以下です。

.tsx
// ネットワーク切り替え時の処理
    const chainChanged = (chain: string) => {
      if (chain !== "0x1") {
        setError("イーサリアムメインネットでのみ利用可能です");
        return;
      }
      if (!window.ethereum) {
        return;
      }
      window.ethereum
        .request({ method: "eth_accounts" })
        .then((accounts: any) => {
          if (accounts.length !== 0) {
            setAddress(accounts[0]);
          } else {
            setError("認証済みのアカウントが見つかりませんでした");
          }
        });
    };
    window.ethereum.on("chainChanged", chainChanged);

src/utils/wallet/InitializeAccount.tsxのuseEffect内に追加します。

src/utils/wallet/InitializeAccount.tsx
//...省略
  useEffect(() => {
    if (!window.ethereum || !window.ethereum.isMetaMask) {
      setError("Metamaskをインストールしてください");
      return;
    }

    // アカウント切り替え時の処理
    const accountChanged = async (newAccount: string) => {
      setAddress(newAccount[0]);
    };
    window.ethereum.on("accountsChanged", accountChanged);
    
    // ネットワーク切り替え時の処理
    const chainChanged = (chain: string) => {
      if (chain !== "0x1") {
        setError("イーサリアムメインネットでのみ利用可能です");
        return;
      }
      if (!window.ethereum) {
        return;
      }
      window.ethereum
        .request({ method: "eth_accounts" })
        .then((accounts: any) => {
          if (accounts.length !== 0) {
            setAddress(accounts[0]);
          } else {
            setError("認証済みのアカウントが見つかりませんでした");
          }
        });
    };
    window.ethereum.on("chainChanged", chainChanged);

    window.ethereum
      .request({ method: "eth_accounts" })
      .then((accounts: any) => {
        if (window.ethereum?.chainId !== "0x1") {
          setError("イーサリアムメインネットでのみ利用可能です");
          return;
        }
        if (accounts.length !== 0) {
          setAddress(accounts[0]);
        }
      });
  }, [setAddress, setError]);
//...省略

window.ethereum.on("chainChanged", chainChanged); でネットワークを切り替えた時に、イーサリアムメインネット以外であれば、アカウントを削除して、エラーを設定しています。

これでネットワークの切り替えにも対応できました。

🎉👏

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?