💬

Next.jsで作るオリジナルSolanaウォレットアダプター

2025/02/24に公開

はじめに

前回の記事ではSolana公式のウォレットアダプターを紹介しました。

https://zenn.dev/devinoue/articles/35f6db0474d4b7

かなり多くのSolanaのサイトがこちらをそのまま使用しているところを見ると、デザインも手堅く癖がなく、どんなサイトでも利用できそうなコンポーネントではないかと思います。

とはいえ、サイトデザインによってはイメージと合わないということもあります

その場合は以下のように読み込まれたcssファイルがあるのですが、

import "@solana/wallet-adapter-react-ui/styles.css";

ここに書かれたスタイルをオーバーライドする必要があり、少し面倒です。

また、それ以上に複雑な改変があるときは、デフォルトで用意されている@solana/wallet-adapter-react-uiでは適合しないことがあるでしょう。

今回はオリジナルのマルチボタンを作ることで、もう少し自由度のあるカスタム・ウォレットアダプターを作りたいと思います。

パッケージの解説

ウォレットアダプターは以下の5つのパッケージのインストールを要求することが多いです

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

Solanaウォレットアダプターは、Reactだけでなく様々なフロントエンド・フレームワークと統合可能にするため、モジュラー式にコード分離がされています。そのため何がどういう意味があって分離されているのか、分かりにくいところがあると思います。突然5つの依存が追加されるため、最初は戸惑うことでしょう。

@solana/web3.jsは必須パッケージのため別として、残りの4つは以下のような機能を持つパッケージになります。

パッケージ名 機能
@solana/wallet-adapter-base TypeScriptでウォレットアダプターを使用するための基本パッケージ。ユーザーがインストールしたウォレットを検出し、ウォレットからトランザクションに署名/送信する関数が含まれています
@solana/wallet-adapter-react Reactでウォレットアダプターを利用するためのコンテキストとフックを含むパッケージ。useConnectionやuseWalletのようなフックがあります
@solana/wallet-adapter-base-ui react-ui、MUI、Ant designなどで使うためのReact UI共通パッケージ。@solana/wallet-adapter-reactに依存しています。
@solana/wallet-adapter-react-ui Reactですぐに使用できるようなUIコンポーネントのパッケージです。

今回はこのうち、@solana/wallet-adapter-react-uiを使わずに、カスタムウォレットアダプターを作ってみましょう。

その気になればもっと減らせますが、代わりのコード量が増えるという問題があり、なかなか難しいところがあります。

ところでVueコンポーネントパッケージはないのですが、取引量の多いSolanaアプリケーション上位30位のうちウェブサイトで90%以上はReactサイトです

以前はRaydiumなどNuxtを使っているサイトも見られましたが、多くはNextに移行しています。

逆にReactを使っていないのはJPoolで、彼らのみVueを現在も使い続けていますし、よく管理されているように見えます。Vueは公式でサポートされていないため、苦労が伺えます。

マルチボタンとは?

他の分野ではあまり聞かないコンポーネントですが、仮想通貨界隈ではよく使われます。

ウォレット接続前はウォレット一覧を表示するモーダルを開くためのボタンであり、接続後は(たいてい)メニューボタンになるコンポーネントです。接続後は現在のウォレットアドレスの一部や、所持しているSOLなどを表示したり、クリックするとメニューが開きアドレスのコピーやdisconnetが出来るようになります。

マルチボタンは内部的にはContext APIを使い自身の状態を管理しています。

wallet-adapter-react-uiを使わずに、マルチボタンを実装する

さて、今回の目標は依存を一つ減らすということです
wallet-adapter-react-uiは手軽にマルチボタンを実装できる反面、スタリングの自由度に欠けるので、こちらをあらかじめ用意されているuseWalletMultiButtonを使って実装することにします。

ブラウザから確認されたSolanaウォレットスタンダード準拠のウォレットはWallet型のデータとしてまとめられ、接続ボタン押下時に指定のフックで取得できます。

前回のコードを流用しますが、1つ依存が減るのでインストールするとなると以下のようになります。

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

公式のexampleを参考にしていますが、コードが微妙に間違っているため修正したものを掲載しておきます(修正PRは出していますが今のところ反応なし……)。

前回のコードを流用し、以下の場所に新しいファイルを作ります

├── src
│   └── app
│       ├── components
│       │   ├── AppWalletProvider.tsx
│       │   └── CustomMultiButton.tsx // ★新規作成
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.tsx
│       └── page.tsx

内容は以下の通りです。

import { WalletName } from "@solana/wallet-adapter-base";
import { useWalletMultiButton } from "@solana/wallet-adapter-base-ui";
import { Wallet } from "@solana/wallet-adapter-react";
import { useCallback, useState } from "react";

export function CustomMultiButton() {
  const [walletModalConfig, setWalletModalConfig] = useState<{
    onSelectWallet(walletName: WalletName): void;
    wallets: Wallet[];
  } | null>(null);

  const { buttonState, onConnect, onDisconnect, onSelectWallet } =
    useWalletMultiButton({
      onSelectWallet({ onSelectWallet, wallets }) {
        setWalletModalConfig({ onSelectWallet, wallets });
      },
    });
  let label;
  switch (buttonState) {
    case "connected":
      label = "Disconnect";
      break;
    case "connecting":
      label = "Connecting";
      break;
    case "disconnecting":
      label = "Disconnecting";
      break;
    case "has-wallet":
      label = "Connect";
      break;
    case "no-wallet":
      label = "Select Wallet";
      break;
  }
  const handleClick = useCallback(() => {
    switch (buttonState) {
      case "connected":
        onDisconnect?.();
        break;
      case "connecting":
      case "disconnecting":
        break;
      case "has-wallet":
        onConnect?.();
        break;
      case "no-wallet":
        onSelectWallet?.();
        break;
    }
  }, [buttonState, onDisconnect, onConnect, onSelectWallet]);

  return (
    <>
      <button
        disabled={
          buttonState === "connecting" || buttonState === "disconnecting"
        }
        onClick={handleClick}
      >
        {label}
      </button>
      {walletModalConfig ? (
        <div className="flex flex-col gap-4">
          {walletModalConfig.wallets.map((wallet) => (
            <button
              key={wallet.adapter.name}
              onClick={() => {
                walletModalConfig.onSelectWallet(wallet.adapter.name);
                setWalletModalConfig(null);
              }}
            >
              {wallet.adapter.name}
            </button>
          ))}
        </div>
      ) : null}
    </>
  );
}

useWalletMultiButtonの引数に接続時のイベントハンドラ(onSelectWallet)を渡します。

コード内では分かりやすさのためにウォレットの表示を雑にしていますが、実際のコードではモーダルなどを使って表示するといいでしょう。

表示するpage.tsxも書き換えます

"use client";

import Link from "next/link";
import { CustomMultiButton } from "./components/CustomMultiButton";

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={{}} /> */}
        <CustomMultiButton />
      </div>
      <Link href="/address">To address</Link>
    </main>
  );
}

さらに、モーダルももう使っていませんのでこのプロバイダーが不要になります。
AppWalletProviderから以下のうよにモーダルプロバイダーを削除できます


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


前回のUIと違い、ただ文字列と最低限のbutton要素だけ入れています。これを他のヘッドレスUIコンポーネントと組み合わせれば、いい感じにのウォレットアダプターを作ることができるでしょう。

終わりに

今回は手軽にマルチボタンの実装ができるuseWalletMultiButtonフックを使ってみました。これで以前よりも依存が少なくなり、ヘッドレスUIとして実装することができます。

何か間違いなどありましたらコメントして頂けると助かります。

Discussion