🔗
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)を直接呼び出してウォレット接続を実装します。 eth_requestAccounts でユーザーの承認を要求し、eth_chainId で接続中のネットワークを取得します。 接続状態は4つのステート(no-wallet / disconnected / connecting / connected)を CSS クラスで管理します。 accountsChanged / chainChanged イベントをリッスンし、ウォレット側の変更をリアルタイムで反映します。 外部ライブラリは不要で、window.ethereum が存在しない場合(未インストール)はインストール誘導メッセージを表示します。

B-001 calls window.ethereum (MetaMask / EIP-1193 API) directly to implement wallet connection. eth_requestAccounts requests user approval; eth_chainId retrieves the connected network. 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. No external libraries required. When window.ethereum is undefined (wallet not installed), a prompt to install is shown.

調整可能パラメータ 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');
    const [address] = await window.ethereum.request({ method: 'eth_requestAccounts' });
    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');
      const [addr] = await window.ethereum.request({ method: 'eth_requestAccounts' });
      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; }

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