🐷

Next.jsで始めるSolanaウォレットアダプター実装ガイド

2025/02/23に公開

はじめに

Solanaブロックチェーンとブラウザから相互作用可能にするためには、まず第一にウォレット拡張機能が必要です。しかしその拡張機能を使うには、フロントエンドで側でウォレットを選択させるためのウォレットアダプターというものが必要になります。

Solanaではwallet-adapterというウォレットアダプター用パッケージを用意されており、もともとSolana labsが開発していたのですが、現在はAnzaが管理しています。

今回はReactエコシステムで最もよく使われているメタフレームワークであるNext.jsを使って、Solanaウォレットアダプターの解説をしていきます。

公式の解説もありますが、Solana財団のガイドの方がよりモダンな解説があるため、こちらを参考にしながら進めたいと思います。

セットアップ方法

今回のチュートリアルではあらかじめNode.jsもインストールしておいてください。ただ慣れていれば、別のパッケージマネージャーでも問題ないです。

create-next-appを使いNext.jsプロジェクトを作ります。基本はデフォルトの選択でOKです。Solana財団のガイドでも使用しているのでtailwindも入れておきます。

npx create-next-app@latest

次にSolanaウォレットアダプターをインストールします。

今回は既存のUIコンポーネントを使用するのですが、必要最低限のパッケージでも以下のように5つ必要になります。

(公式に書かれているreactはインストール済みなので不要です)

npm install @solana/web3.js@1 \
    @solana/wallet-adapter-base \
    @solana/wallet-adapter-react \
    @solana/wallet-adapter-react-ui \
    @solana/wallet-adapter-wallets

Providerを作る

walletの状態管理はContext APIを使用するため、プロバイダーが必要になります。

ガイドに従い、componentsというディレクトリを作り、AppWalletProvider.tsxという名前のファイルを追加します。

├── src
│   └── app
│       ├── components
│       │   └── AppWalletProvider.tsx
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.tsx
│       └── page.tsx

まずは以下のように書きましょう。

"use client";

import React, { useMemo } from "react";
import {
  ConnectionProvider,
  WalletProvider,
} from "@solana/wallet-adapter-react";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { clusterApiUrl } from "@solana/web3.js";
// import { UnsafeBurnerWalletAdapter } from "@solana/wallet-adapter-wallets";

// Default styles that can be overridden by your app
import "@solana/wallet-adapter-react-ui/styles.css";

このファイルはコンテキストプロバイダーとして、子コンポーネントすべてでアクセスできるデータを提供するためのものです。

use clientディレクティブが先頭についていますが、これはContext APIはクライアントサイドのコンポーネントになるため宣言します。

ウォレットアダプターには標準でexportされているプロバイダーが2つあり、どちらも使うのでここで一緒にimportします。

  • WalletAdapterNetwork
    • mainnet-beta、testnet、devnetなどのデフォルトのネットワーク値を定義するenum
  • WalletModalProvider
    • @solana/wallet-adapter-baseから提供され、「ウォレット接続」ボタンのコンテキストを持っている
    • ウォレット接続モーダルコンポーネントの表示を切り替えるのに役立ち、ユーザーがアプリケーションに接続するSolanaウォレットを選択できるようにする
    • これもモーダルに使うだけなので、別のモーダルを使うのであれば本来なくても動きます
  • UnsafeBurnerWalletAdapter
    • ウォレットのテストのためにウォレットアダプターのメンテナーによって提供されるReactコンポーネント。要はバーナーウォレット
    • スターターパックで使用されるデフォルトのウォレットアダプター
    • ウォレットアダプターインターフェースを実装していますが、メッセージの署名に安全でないローカルキーペアを使用する
    • ただし本番アプリケーションでは使用しないと思う

さらに今回はAnzaが用意している標準ウォレットアダプターのUIコンポーネントを使用するため、これらのためのCSSスタイルもインポートする必要があります。

よって標準ウォレットアダプターをカスタムするには、CSSクラスをオーバーライドする必要があります。

サポートされているウォレットと、ウォレットスタンダード

ユーザーがアプリケーション内で使用したい様々なSolanaウォレットをサポートするには、2つの方法があります:

  1. wallet-standard
    1. これらのウォレットは@solana/wallet-adapter-base で自動的に検出され、ユーザーが使用するためにアプリケーションに追加のコードを追加する必要はありません。
  2. レガシーウォレットアダプター を持つウォレット
    1. npmパッケージとしてバンドルされています。 アプリケーション内でこれらのウォレットをサポートするには、それぞれのウォレットのレガシーアダプタをアプリケーションにインストールしてimportする必要があります。

基本的に現代のSolanaウォレットはwallet-standardに準拠して実装されています。

そのためアプリケーション内で特別なことをする必要はありません。 自動的に機能し、ユーザーに表示されます(Solana Mobile Stack Wallet Adapter も含め)。

ユーザーがアプリケーションにサポートしてほしい特定の非標準ウォレットがある場合は、そのレガシーアダプターを手動で追加する、という感じになります。

AppWalletProvider.tsxの続きです。

export default function AppWalletProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const network = WalletAdapterNetwork.Devnet;
  const endpoint = useMemo(() => clusterApiUrl(network), [network]);
  const wallets = useMemo(
    () => [
      // 必要であればレガシーウォレットアダプターをここに手動で追加してください
      // new UnsafeBurnerWalletAdapter(),
    ],
    [network],
  );

  return (
    <ConnectionProvider endpoint={endpoint}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>{children}</WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
}

AppWalletProviderコンポーネントは、Solanaネットワーククラスター(devnet、mainnet-betaなど)への接続と、選択したRPCエンドポイントに使用されます。RPCエンドポイントは、アプリケーションがトランザクションを送信する先となります。今回のガイドでは、devnetネットワーククラスターを使用するようにハードコードしています。

またwalletsという配列も定義しています。この配列には、アプリケーション内でインポートしサポートしたいレガシーウォレットアダプターウォレットが含めています。

ただ、通常はこの配列にエントリを追加する必要はないです。おそらくデバッグ的に使うことはあるでしょうが、空でOKです。よってnetworkという依存配列の要素も本来不要ですが、クラスタを変更したいというケースがある場合にこれを使います。

endpointとwalletの変数をウォレットアダプターパッケージから提供されるConnectionProviderとWalletProviderにpropsとして渡します。

autoConnectプロップは、アプリケーションの読み込み時にユーザーの以前に接続されたウォレットに自動的に接続を試みるかどうかを宣言する設定です。

WalletModalProviderには、WalletModalというコンポーネントがあり、これはユーザーが環境(つまりウェブブラウザ)で利用可能かつインストールされているウォレットのリスト表示に使います。

アプリケーションをAppWalletAdapterでラップする

出来上がったAppWalletAdapterコンテキストでNextアプリケーションをラップしましょう。アプリケーションの他の部分や子ページ内でそのAPI、フック、状態にアクセスできるようになります。

ルートのlayout.tsxファイル内で、AppWalletAdapterプロバイダーをインポートし、アプリケーションの子要素をAppWalletAdapterコンポーネントの子要素として渡します:

import AppWalletProvider from "./components/AppWalletProvider";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <AppWalletProvider>{children}</AppWalletProvider>
      </body>
    </html>
  );
}

これでこのNextプロジェクト上でウォレットを使用する準備が整いました。

ウォレット接続ボタン

「ウォレット接続」ボタンとは、よくDeFiサイトの右上にあるボタンです。押下すると「ウォレット選択モーダル」を開き、接続したいSolanaウォレットを選択できるようにします。

通常、開発者はこのウォレット接続ボタンコンポーネントをメインヘッダーコンポーネントに追加し、他のすべてのページでも接続ボタンが表示され、アクセス可能になるようにします。

今回のガイドではとりあえずトップページにウォレット接続ボタンを追加します

アプリフォルダのルートにあるpage.tsxに、@solana/wallet-adapter-react-uiからWalletMultiButtonをインポートします。その後、トップのページコンポーネントからそのボタンコンポーネントを表示します。

page.tsxに元あるコードをすべて以下のコードに置き換えてください。

"use client";

import { WalletMultiButton } from "@solana/wallet-adapter-react-ui";

export default function Home() {
  return (
    <main className="flex items-center justify-center min-h-screen">
      <div className="border hover:border-slate-900 rounded">
        <WalletMultiButton style={{}} />
      </div>
    </main>
  );
}

開発サーバーを起動します

npm run dev

画面中央にボタンが表示されると思います。これで任意のStandardなウォレットと接続すれば、以下のように公開鍵が表示されます。

ページ上のウォレットとインタラクションする

このような関数や、ウォレットアダプターパッケージ内で提供される関数を使用することで、ユーザーのウォレットが接続されているかどうかを検出したり、定義されたネットワーク内のdevnetやSOLのエアドロップを取得するボタンを作成したりすることができます。さらに多くのことが可能です。

次に、これらのフックを使用して実際に接続オブジェクトにアクセスし、ユーザーのウォレット状態を利用して取引の送信や署名、ウォレット残高の読み取り、機能のテストを行う方法を示すために、別のページを作成しましょう。

appフォルダ内に新しいフォルダを作成し、addressという名前を付けます。そのフォルダ内にpage.tsxファイルを作成します。この例では、新しいページは src/app/address/page.tsx に配置されます:

├── src
│   └── app
│       ├── address
│       │   └── page.tsx
│       ├── components
│       │   └── AppWalletProvider.tsx
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.tsx
│       └── page.tsx

新しいページの内部で、上記で議論したフックとカスタム関数を利用して、ユーザーが選択したSolanaウォレットとこれらのインタラクションを実際に行えるようにすることができます。

とりあえず仮置きとして以下のようにpage.tsxを埋めておきましょう。

"use client";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import { useEffect, useState } from "react";

export default function Address() {
  const { connection } = useConnection();
  const { publicKey } = useWallet();
  const [balance, setBalance] = useState<number>(0);

  // code for the `getAirdropOnClick` function here

  // code for the `getBalanceEvery10Seconds` and useEffect code here

  return (
    <main className="flex min-h-screen flex-col items-center justify-evenly p-24">
      {publicKey ? (
        <div className="flex flex-col gap-4">
          <h1>Your Public key is: {publicKey?.toString()}</h1>
          <h2>Your Balance is: {balance} SOL</h2>
          <div>
            <button
              onClick={getAirdropOnClick}
              type="button"
              className="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
            >
              Get Airdrop
            </button>
          </div>
        </div>
      ) : (
        <h1>Wallet is not connected</h1>
      )}
    </main>
  );
}

エアドロップ・インタラクション

アプリケーションがユーザーのSolanaウォレットとインタラクションする最も一般的な方法は、AppWalletAdapterコンテキストのすべての子コンポーネントからアクセス可能なuseWalletとuseConnectionフックを使用することです。

useWalletフックには、公開鍵やウォレットの状態、接続中か接続済みかなどの詳細が含まれています。useConnectionフックは、RPCエンドポイントを介してアプリケーションをSolanaブロックチェーンに接続することを容易にします。

以下は、useConnectionとuseWalletフックから接続と公開鍵を使用して、devnet SOLのエアドロップを取得する例です。

このgetAirdropOnClick関数は、getLatestBlockhash RPCメソッドを使用して最新のブロックハッシュを取得します。また、接続のrequestAirdrop関数からトランザクションの署名を取得します。トランザクションが確認されてブロックに追加されたかどうかを確認することもでき、エアドロップが成功し、SOLが使用可能であることを確認できます。

const { connection } = useConnection();
const { publicKey } = useWallet();

const getAirdropOnClick = async () => {
  try {
    if (!publicKey) {
      throw new Error("Wallet is not Connected");
    }
    const [latestBlockhash, signature] = await Promise.all([
      connection.getLatestBlockhash(),
      connection.requestAirdrop(publicKey, 1 * LAMPORTS_PER_SOL),
    ]);
    const sigResult = await connection.confirmTransaction(
      { signature, ...latestBlockhash },
      "confirmed",
    );
    if (sigResult) {
      alert("Airdrop was confirmed!");
    }
  } catch (err) {
    alert("You are Rate limited for Airdrop");
  }
};

そして現在のSOLについては、少し奇妙ですが、useEffectを仕掛けて定期チェックするという方法を取ります。

const [balance, setBalance] = useState<number>(0);

useEffect(() => {
  if (publicKey) {
    (async function getBalanceEvery10Seconds() {
      const newBalance = await connection.getBalance(publicKey);
      setBalance(newBalance / LAMPORTS_PER_SOL);
      setTimeout(getBalanceEvery10Seconds, 10000);
    })();
  }
}, [publicKey, connection, balance]);

上記コードを書き加えると、address/page.tsxの中身は全体として以下のようになります。

"use client";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import { useEffect, useState } from "react";

export default function Address() {
  const { connection } = useConnection();
  const { publicKey } = useWallet();
  const [balance, setBalance] = useState<number>(0);

  // code for the `getAirdropOnClick` function here
  const getAirdropOnClick = async () => {
    try {
      if (!publicKey) {
        throw new Error("Wallet is not Connected");
      }
      const [latestBlockhash, signature] = await Promise.all([
        connection.getLatestBlockhash(),
        connection.requestAirdrop(publicKey, 1 * LAMPORTS_PER_SOL),
      ]);
      const sigResult = await connection.confirmTransaction(
        { signature, ...latestBlockhash },
        "confirmed",
      );
      if (sigResult) {
        alert("Airdrop was confirmed!");
      }
    } catch (err) {
      if (err instanceof Error) alert("You are Rate limited for Airdrop");
    }
  };
  // code for the `getBalanceEvery10Seconds` and useEffect code here
  useEffect(() => {
    if (publicKey) {
      (async function getBalanceEvery10Seconds() {
        const newBalance = await connection.getBalance(publicKey);
        setBalance(newBalance / LAMPORTS_PER_SOL);
        setTimeout(getBalanceEvery10Seconds, 10000);
      })();
    }
  }, [publicKey, connection, balance]);
  return (
    <main className="flex min-h-screen flex-col items-center justify-evenly p-24">
      {publicKey ? (
        <div className="flex flex-col gap-4">
          <h1>Your Public key is: {publicKey?.toString()}</h1>
          <h2>Your Balance is: {balance} SOL</h2>
          <div>
            <button
              onClick={getAirdropOnClick}
              type="button"
              className="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
            >
              Get Airdrop
            </button>
          </div>
        </div>
      ) : (
        <h1>Wallet is not connected</h1>
      )}
    </main>
  );
}

/addressルートに直接URLを叩くとContextが失われるため、トップページからアクセスできるようにしましょう。

app/page.tsxは以下のように丸ごと書き換えます。

"use client";

import { WalletMultiButton } from "@solana/wallet-adapter-react-ui";
import Link from "next/link";

export default function Home() {
  return (
    <main className="flex items-center justify-center min-h-screen flex-col gap-4">
      <div className="border hover:border-slate-900 rounded">
        <WalletMultiButton style={{}} />
      </div>
      <Link href="/address">To address</Link>
    </main>
  );
}

これで、以下のようにaddressに遷移すればSOLのエアドロップが可能になります。

終わりに

多少インストールするパッケージが多いですが、これはウォレットアダプターが様々なフロントエンドフレームワークに対応するためのものと思われます。
また今回はあらかじめ準備されたウォレットアダプターを使っていますが、オリジナルのウォレットアダプターも作れるので、次回以降にそちらをご紹介できればと思っています。

参考

Add Solana Wallet Adapter to a NextJS application

Discussion