🔗
B: Blockchain / Web3 カテゴリは、ウォレットや Ethereum との連携に特化したパターン集です。 基本(B-001〜)window.ethereum のみで動作しライブラリ不要。 α 構成(B-002〜) では ethers.js(CDN)を使用し、残高取得・署名・コントラクト呼び出しを扱います。 デモには MetaMask または EIP-1193 対応ウォレットが必要です。 B: Blockchain / Web3 category — patterns for wallet and Ethereum integration. Basic (B-001+) uses only window.ethereum, no libraries required. α builds (B-002+) add ethers.js via CDN for balance, signing, and contract calls. Requires MetaMask or an EIP-1193 compatible wallet to run the live demo.

ライブデモ Live Demo

MetaMask(または EIP-1193 対応ウォレット)が必要です。接続・ネットワーク表示・アドレスコピー・切断をデモします。

Requires MetaMask or an EIP-1193 wallet. Demonstrates connect, network display, address copy, and disconnect.

ウォレットが検出されませんでした。
MetaMask をインストールして再読み込みしてください。
No wallet detected.
Please install MetaMask and reload.

接続中... Connecting...

ウォレットの承認を待っています

Waiting for wallet approval

0x0000...0000

AI向け説明 AI Description

B-001window.ethereum(MetaMask / EIP-1193 API)を直接呼び出してウォレット接続を実装します。 Connect ボタンでは wallet_requestPermissions を使ってアカウント選択ポップアップを毎回強制表示します(eth_requestAccounts は承認済みサイトではダイアログを出さないため)。 切断意思は localStorage フラグで記憶し、ページリロード後のサイレント再接続を防ぎます。 接続状態は4つのステート(no-wallet / disconnected / connecting / connected)を CSS クラスで管理します。 accountsChanged / chainChanged イベントをリッスンし、ウォレット側の変更をリアルタイムで反映します。

B-001 calls window.ethereum (MetaMask / EIP-1193 API) directly to implement wallet connection. The Connect button uses wallet_requestPermissions to force the account picker every time (eth_requestAccounts returns silently when the site is already approved). Disconnect intent is persisted in localStorage to prevent silent auto-reconnect on page reload. Connection state is managed with four states (no-wallet / disconnected / connecting / connected) via CSS class toggling. accountsChanged and chainChanged events are listened to for real-time wallet updates.

調整可能パラメータ Adjustable Parameters

実装 Implementation

HTML + CSS + JS

<!-- State containers -->
<div id="state-no-wallet" class="wallet-state">
  <p>MetaMask not found. <a href="https://metamask.io/" target="_blank">Install</a></p>
</div>
<div id="state-disconnected" class="wallet-state active">
  <button id="btn-connect">🦊 Connect Wallet</button>
</div>
<div id="state-connecting" class="wallet-state">
  <span class="spinner"></span> Connecting...
</div>
<div id="state-connected" class="wallet-state">
  <span id="network-display"></span>
  <span id="address-display"></span>
  <button id="btn-copy">copy</button>
  <button id="btn-disconnect">Disconnect</button>
</div>

<style>
.wallet-state { display: none; }
.wallet-state.active { display: flex; flex-direction: column; align-items: center; gap: 12px; }

.btn-connect {
  padding: 12px 28px;
  background: linear-gradient(135deg, #f59e0b, #d97706);
  color: #fff;
  border: none;
  border-radius: 12px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
  width: 18px; height: 18px;
  border: 2px solid rgba(245,158,11,.25);
  border-top-color: #f59e0b;
  border-radius: 50%;
  animation: spin .7s linear infinite;
}
</style>

<script>
const CHAIN_NAMES = {
  '0x1': 'Ethereum', '0xaa36a7': 'Sepolia', '0x89': 'Polygon',
  '0x2105': 'Base', '0xa': 'Optimism', '0xa4b1': 'Arbitrum', '0x38': 'BNB Chain',
};

const states = {
  'no-wallet':    document.getElementById('state-no-wallet'),
  'disconnected': document.getElementById('state-disconnected'),
  'connecting':   document.getElementById('state-connecting'),
  'connected':    document.getElementById('state-connected'),
};

function showState(name) {
  Object.entries(states).forEach(([k, el]) => el.classList.toggle('active', k === name));
}

if (!window.ethereum) {
  showState('no-wallet');
} else {
  showState('disconnected');
  window.ethereum.request({ method: 'eth_accounts' })
    .then(accs => { if (accs.length) getChainAndShow(accs[0]); });
}

document.getElementById('btn-connect').addEventListener('click', async () => {
  try {
    showState('connecting');
    // wallet_requestPermissions forces account picker every time
    await window.ethereum.request({
      method: 'wallet_requestPermissions',
      params: [{ eth_accounts: {} }],
    });
    const [address] = await window.ethereum.request({ method: 'eth_accounts' });
    await getChainAndShow(address);
  } catch { showState('disconnected'); }
});

document.getElementById('btn-disconnect').addEventListener('click', () => showState('disconnected'));

document.getElementById('btn-copy').addEventListener('click', async function () {
  await navigator.clipboard.writeText(this.dataset.address);
  this.textContent = 'copied!';
  setTimeout(() => this.textContent = 'copy', 1500);
});

async function getChainAndShow(address) {
  const chainId = await window.ethereum.request({ method: 'eth_chainId' });
  document.getElementById('address-display').textContent = address.slice(0,6) + '...' + address.slice(-4);
  document.getElementById('btn-copy').dataset.address = address;
  document.getElementById('network-display').textContent = CHAIN_NAMES[chainId] ?? `Chain ${parseInt(chainId, 16)}`;
  showState('connected');
}

window.ethereum?.on('accountsChanged', accs =>
  accs.length ? getChainAndShow(accs[0]) : showState('disconnected')
);
window.ethereum?.on('chainChanged', () =>
  window.ethereum.request({ method: 'eth_accounts' })
    .then(accs => accs.length && getChainAndShow(accs[0]))
);
</script>

React (JSX + CSS)

// react/B-001.jsx
import { useState, useEffect, useCallback } from 'react';
import './B-001.css';

const CHAIN_NAMES = {
  '0x1': 'Ethereum', '0xaa36a7': 'Sepolia', '0x89': 'Polygon',
  '0x2105': 'Base', '0xa': 'Optimism', '0xa4b1': 'Arbitrum', '0x38': 'BNB Chain',
};

export default function WalletConnectButton() {
  const [status, setStatus] = useState('init'); // init | no-wallet | disconnected | connecting | connected
  const [address, setAddress] = useState('');
  const [network, setNetwork] = useState('');
  const [copied, setCopied] = useState(false);

  const showConnected = useCallback(async (addr) => {
    const chainId = await window.ethereum.request({ method: 'eth_chainId' });
    setAddress(addr);
    setNetwork(CHAIN_NAMES[chainId] ?? `Chain ${parseInt(chainId, 16)}`);
    setStatus('connected');
  }, []);

  useEffect(() => {
    if (!window.ethereum) { setStatus('no-wallet'); return; }
    setStatus('disconnected');
    window.ethereum.request({ method: 'eth_accounts' })
      .then(accs => { if (accs.length) showConnected(accs[0]); })
      .catch(() => {});

    const onAccounts = accs =>
      accs.length ? showConnected(accs[0]) : setStatus('disconnected');
    const onChain = () =>
      window.ethereum.request({ method: 'eth_accounts' })
        .then(accs => { if (accs.length) showConnected(accs[0]); })
        .catch(() => {});

    window.ethereum.on('accountsChanged', onAccounts);
    window.ethereum.on('chainChanged', onChain);
    return () => {
      window.ethereum.removeListener('accountsChanged', onAccounts);
      window.ethereum.removeListener('chainChanged', onChain);
    };
  }, [showConnected]);

  async function connect() {
    try {
      setStatus('connecting');
      await window.ethereum.request({
        method: 'wallet_requestPermissions',
        params: [{ eth_accounts: {} }],
      });
      const [addr] = await window.ethereum.request({ method: 'eth_accounts' });
      await showConnected(addr);
    } catch { setStatus('disconnected'); }
  }

  async function copyAddress() {
    await navigator.clipboard.writeText(address);
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  }

  const truncated = address ? address.slice(0, 6) + '...' + address.slice(-4) : '';

  return (
    <div className="wallet-demo">
      {status === 'no-wallet' && (
        <p className="no-wallet-msg">
          No wallet detected.{' '}
          <a href="https://metamask.io/" target="_blank" rel="noopener">Install MetaMask</a>
        </p>
      )}
      {status === 'disconnected' && (
        <button className="btn-connect" onClick={connect}>🦊 Connect Wallet</button>
      )}
      {status === 'connecting' && (
        <div className="connecting-wrap">
          <span className="w-spinner" /> Connecting...
        </div>
      )}
      {status === 'connected' && (
        <div className="wallet-card">
          <div className="wallet-card-top">
            <span className="connected-dot" />
            <span className="network-name">{network}</span>
          </div>
          <div className="address-row">
            <span className="address-text">{truncated}</span>
            <button
              className={`btn-copy${copied ? ' copied' : ''}`}
              onClick={copyAddress}
            >
              {copied ? 'copied!' : 'copy'}
            </button>
          </div>
          <button className="btn-disconnect" onClick={() => setStatus('disconnected')}>
            Disconnect
          </button>
        </div>
      )}
    </div>
  );
}
/* react/B-001.css */
.wallet-demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 200px;
  padding: 40px 24px;
}

.btn-connect {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 13px 30px;
  background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
  color: #fff;
  border: none;
  border-radius: 12px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35);
  transition: filter 0.15s, transform 0.15s;
}
.btn-connect:hover { filter: brightness(1.08); transform: translateY(-2px); }

.connecting-wrap { display: flex; align-items: center; gap: 10px; color: #888; font-size: 14px; }

@keyframes spin { to { transform: rotate(360deg); } }
.w-spinner {
  display: inline-block;
  width: 18px; height: 18px;
  border: 2px solid rgba(245, 158, 11, 0.25);
  border-top-color: #f59e0b;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

.wallet-card {
  background: #1c1c2a;
  border: 1px solid #2a2a3a;
  border-radius: 14px;
  padding: 20px 24px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-width: 270px;
}
.wallet-card-top { display: flex; align-items: center; gap: 8px; }
.connected-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: #22c55e;
  box-shadow: 0 0 6px rgba(34, 197, 94, 0.6);
}
.network-name { font-size: 13px; color: #888; font-weight: 500; }
.address-row { display: flex; align-items: center; gap: 8px; }
.address-text { font-family: 'Courier New', monospace; font-size: 15px; font-weight: 600; }
.btn-copy {
  padding: 3px 10px;
  background: transparent;
  border: 1px solid #2a2a3a;
  border-radius: 6px;
  font-size: 12px; color: #888;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.btn-copy:hover { background: #2a2a3a; color: #fff; }
.btn-copy.copied { color: #22c55e; border-color: #22c55e; }
.btn-disconnect {
  background: transparent; border: none;
  font-size: 13px; color: #888;
  cursor: pointer; text-decoration: underline;
  transition: color 0.15s;
}
.btn-disconnect:hover { color: #ef4444; }
.no-wallet-msg { font-size: 14px; color: #888; line-height: 1.7; }
.no-wallet-msg a { color: #fbbf24; }

AIへの指示テンプレート AI Prompt Template

以下のテンプレートをコピーしてAIアシスタントに貼り付けると、このパターンの実装を依頼できます。 Copy the template below and paste it into your AI assistant to request an implementation of this pattern.

注意とバリエーション Notes & Variations