MetaMask logo

Skill

setup-solana-react-app

integrate MetaMask with Solana in React

Covers React Web3 Solana Frontend

Description

Set up a React app with @metamask/connect-solana and the Solana wallet adapter. Use when integrating MetaMask with Solana in React, configuring WalletProvider, or building connect/sign/send flows with useWallet.

SKILL.md

Setup Solana React App with MetaMask

When to use

Use this skill when:

  • Integrating MetaMask with Solana in a React web application
  • Configuring @solana/wallet-adapter-react with MetaMask Connect's wallet-standard discovery
  • Building connect, sign message, or send transaction flows using useWallet
  • The MetaMask wallet is not appearing in the Solana wallet adapter

Workflow

Step 1: Install dependencies

npm install @metamask/connect-solana @metamask/connect-multichain @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-base @solana/web3.js

@metamask/connect-multichain is a regular dependency of @metamask/connect-solana and is installed transitively. (Only the 2.0.0 release briefly made it a peer dependency; 2.1.0 reverted that.) Installing it explicitly is harmless but not required. The SDK warns at runtime if duplicate or mismatched copies are resolved.

Step 2: Create the Solana client early in app startup

Initialize createSolanaClient early (e.g. in your bootstrap before rendering). It does not need to resolve before the first WalletProvider render — wallet-standard supports late registration, and the SDK registers the wallet about 1 second after the factory resolves. The adapter picks it up via the wallet-standard register event whenever it lands. If your UI asserts "MetaMask is available" synchronously, gate that specific UI on the client being ready.

// src/main.tsx (or index.tsx)
import { createSolanaClient } from '@metamask/connect-solana';
import { createRoot } from 'react-dom/client';
import App from './App';

async function bootstrap() {
  await createSolanaClient({
    dapp: {
      name: 'My Solana DApp',
      url: window.location.href,
    },
  });

  const root = createRoot(document.getElementById('root')!);
  root.render(<App />);
}

bootstrap();

createSolanaClient options:

ParameterTypeDescription
dapp.namestringRequired. Display name of your dApp
dapp.urlstringOptional. dApp URL
dapp.iconUrlstringOptional. dApp icon
api.supportedNetworksPartial<Record<'mainnet' | 'devnet' | 'testnet', string>>Map network names to RPC URLs
debugbooleanReserved — accepted in the options type but not currently forwarded by createSolanaClient (no effect yet)
skipAutoRegisterbooleanSkip automatic wallet-standard registration
analytics.integrationTypestringOptional. Tag analytics events with an integration identifier (added in @metamask/connect-solana 0.8.0)

Solana CAIP chain IDs:

NetworkCAIP ID
Mainnetsolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
Devnetsolana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1
Testnetsolana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z

Step 3: Configure the WalletProvider

Pass an empty wallets array. MetaMask registers via the wallet-standard auto-discovery protocol and must not be added manually.

// src/App.tsx
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import { clusterApiUrl } from '@solana/web3.js';
import '@solana/wallet-adapter-react-ui/styles.css';
import { SolanaDemo } from './SolanaDemo';

const endpoint = clusterApiUrl('mainnet-beta');

export default function App() {
  return (
    <ConnectionProvider endpoint={endpoint}>
      <WalletProvider wallets={[]} autoConnect>
        <WalletModalProvider>
          <SolanaDemo />
        </WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
}

Step 4: Find the MetaMask wallet

When using useWallet, you can verify MetaMask is connected by checking the wallet name. The wallet-standard name is exactly "MetaMask" (case-sensitive).

import { useWallet } from '@solana/wallet-adapter-react';

const { wallet } = useWallet();
const isMetaMask = wallet?.adapter.name === 'MetaMask';

Step 5: Build the full component

// src/SolanaDemo.tsx
import { useWallet, useConnection } from '@solana/wallet-adapter-react';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import {
  Transaction,
  SystemProgram,
  PublicKey,
  LAMPORTS_PER_SOL,
} from '@solana/web3.js';

export function SolanaDemo() {
  const { publicKey, sendTransaction, signMessage, connected, wallet } = useWallet();
  const { connection } = useConnection();

  const handleSignMessage = async () => {
    if (!signMessage) return;
    const message = new TextEncoder().encode('Hello from MetaMask on Solana!');
    try {
      const signature = await signMessage(message);
      console.log('Signature:', Buffer.from(signature).toString('hex'));
    } catch (err) {
      console.error('Sign message failed:', err);
    }
  };

  const handleSendTransaction = async () => {
    if (!publicKey || !sendTransaction) return;
    try {
      const { blockhash } = await connection.getLatestBlockhash();
      const transaction = new Transaction().add(
        SystemProgram.transfer({
          fromPubkey: publicKey,
          toPubkey: new PublicKey('11111111111111111111111111111112'),
          lamports: 0.001 * LAMPORTS_PER_SOL,
        }),
      );
      transaction.recentBlockhash = blockhash;
      transaction.feePayer = publicKey;

      const txSignature = await sendTransaction(transaction, connection);
      const confirmation = await connection.confirmTransaction(txSignature, 'confirmed');
      console.log('Transaction confirmed:', txSignature);
    } catch (err) {
      console.error('Transaction failed:', err);
    }
  };

  return (
    <div>
      <WalletMultiButton />
      {connected && publicKey && (
        <div>
          <p>Wallet: {wallet?.adapter.name}</p>
          <p>Address: {publicKey.toBase58()}</p>
          <button onClick={handleSignMessage}>Sign Message</button>
          <button onClick={handleSendTransaction}>Send 0.001 SOL</button>
        </div>
      )}
    </div>
  );
}

Step 6: Chrome on Android workaround

Chrome on Android has a known bug where the page may unload before MetaMask can respond. Add a beforeunload patch in your entry file:

window.addEventListener('beforeunload', (e) => {
  e.preventDefault();
  e.returnValue = '';
});

Important Notes

  • Initialize createSolanaClient early, but rendering need not wait — wallet registration happens shortly after the factory resolves (the SDK defers it ~1s), and the wallet adapter discovers late registrations via the wallet-standard register event. Only gate UI that assumes MetaMask is already in the wallet list.
  • wallets prop must be [] — MetaMask uses wallet-standard auto-discovery. Passing wallet adapter instances manually will not work and may cause duplicates with other wallets.
  • The wallet name is exactly "MetaMask" — case-sensitive. Use this to identify the MetaMask wallet in the adapter list. Renamed from "MetaMask Connect" in @metamask/connect-solana 1.0.0; since that release, the client will also defer to an already-injected Solana provider (e.g. the MetaMask browser extension) instead of announcing a second "MetaMask" entry.
  • Eager provider initialization + stable getWallet() — since @metamask/connect-solana 1.1.0, createSolanaClient() eagerly initializes the Solana wallet provider during creation; if the underlying multichain session already contains Solana scopes, the provider's accounts are populated by the time the client resolves (no need to wait for wallet_sessionChanged on cold start). getWallet() also returns the same wallet instance on every call now — safe to cache in a useRef / useMemo value, no need to call it on every render.
  • getInfuraRpcUrls helper — use getInfuraRpcUrls({ infuraApiKey: 'YOUR_KEY', networks: ['mainnet', 'devnet'] }) from @metamask/connect-solana to auto-generate supportedNetworks from Infura.
  • Solana networks — mainnet, devnet, and testnet scopes are all modeled by the SDK and wallet-standard layer. Non-mainnet availability depends on the connected MetaMask build/version, so handle connection errors rather than assuming a cluster is present.
  • disconnect() only revokes Solana scopes — if the user also has EVM sessions, those remain active. Each chain family manages its own session lifecycle.
  • Chrome on Android — a known browser bug can interrupt the connection flow. Apply the beforeunload workaround shown in Step 6.

© 2026 YourAI.tools. Every skill from an identity-verified publisher.

Independent catalog. Not affiliated with, endorsed by, or sponsored by Anthropic or any listed publisher. All trademarks belong to their respective owners.