🔗
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 対応ウォレット)が必要です。接続後、ethers.js v6BrowserProvider で ETH 残高をリアルタイム取得します。

Requires MetaMask or an EIP-1193 wallet. After connecting, ETH balance is fetched in real time using ethers.js v6 BrowserProvider.

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

接続中... Connecting...

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

Waiting for wallet approval

0x0000...0000
ETH Balance
...

AI向け説明 AI Description

B-002ethers.js v6(CDN)の BrowserProvider を使い、接続済みウォレットの ETH 残高を取得するパターンです。 provider.getBalance(address) が返す BigInt 値を ethers.formatEther() で小数形式に変換して表示します。 B-001 のウォレット接続ロジックをベースに「残高セクション」を追加した α 構成です。 残高取得は非同期のため、ローディング状態(...)→ 取得完了(x.xxxx ETH)の2段階で表示します。 リフレッシュボタンで任意のタイミングで再取得でき、accountsChanged / chainChanged イベントで自動更新します。

B-002 uses ethers.js v6 (CDN) BrowserProvider to fetch the connected wallet's ETH balance. The BigInt returned by provider.getBalance(address) is converted to a decimal string via ethers.formatEther(). This is an α build extending B-001's wallet connection logic with a "balance section." Because balance fetching is async, it transitions through loading (...) then resolved (x.xxxx ETH) states. A refresh button allows on-demand re-fetching, and accountsChanged / chainChanged events trigger automatic updates.

調整可能パラメータ Adjustable Parameters

実装 Implementation

HTML + CSS + JS

<!-- ethers.js v6 via CDN -->
<script src="https://cdn.jsdelivr.net/npm/ethers@6.7.0/dist/ethers.umd.min.js"></script>

<!-- State containers -->
<div id="state-no-wallet" class="wallet-state">...</div>
<div id="state-disconnected" class="wallet-state active">
  <button id="btn-connect">🦊 Connect Wallet</button>
</div>
<div id="state-connecting" class="wallet-state">...</div>
<div id="state-connected" class="wallet-state">
  <div class="wallet-card">
    <div class="wallet-card-top">
      <span class="connected-dot"></span>
      <span id="network-display"></span>
    </div>
    <div class="address-row">
      <span id="address-display"></span>
      <button id="btn-copy">copy</button>
    </div>
    <div class="balance-section">
      <span class="balance-label">ETH Balance</span>
      <div class="balance-fetch-row">
        <span id="balance-display">...</span>
        <button id="btn-refresh">↻</button>
      </div>
    </div>
    <button id="btn-disconnect">Disconnect</button>
  </div>
</div>

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

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

.wallet-card {
  background: var(--panel-bg); border: 1px solid var(--border);
  border-radius: 14px; padding: 20px 24px;
  display: flex; flex-direction: column; gap: 14px; min-width: 280px;
}
.connected-dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }
.address-text { font-family: monospace; font-size: 15px; font-weight: 600; }

.balance-section { border-top: 1px solid var(--border); padding-top: 14px; display: flex; flex-direction: column; gap: 6px; }
.balance-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
.balance-fetch-row { display: flex; align-items: center; justify-content: space-between; }
.balance-amount { font-size: 22px; font-weight: 700; }
.btn-refresh { padding: 5px 10px; background: transparent; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
</style>

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

const stateEls = {
  '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(stateEls).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) connectAndShow(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 [addr] = await window.ethereum.request({ method: 'eth_accounts' });
    await connectAndShow(addr);
  } catch { showState('disconnected'); }
});

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

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

document.getElementById('btn-refresh').addEventListener('click', () => fetchBalance(currentAddress));

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

async function fetchBalance(address) {
  const el = document.getElementById('balance-display');
  el.textContent = '...';
  try {
    const provider = new ethers.BrowserProvider(window.ethereum);
    const balanceBN = await provider.getBalance(address);
    el.textContent = parseFloat(ethers.formatEther(balanceBN)).toFixed(4) + ' ETH';
  } catch {
    el.textContent = 'Error';
  }
}

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

React (JSX + CSS)

// react/B-002.jsx
// npm install ethers
import { useState, useEffect, useCallback } from 'react';
import { BrowserProvider, formatEther } from 'ethers';
import './B-002.css';

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

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

  const fetchBalance = useCallback(async (addr) => {
    setBalance(null);
    try {
      const provider = new BrowserProvider(window.ethereum);
      const balanceBN = await provider.getBalance(addr);
      setBalance(parseFloat(formatEther(balanceBN)).toFixed(4) + ' ETH');
    } catch {
      setBalance('Error');
    }
  }, []);

  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');
    fetchBalance(addr);
  }, [fetchBalance]);

  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>
          <div className="balance-section">
            <span className="balance-label">ETH Balance</span>
            <div className="balance-fetch-row">
              <span className={`balance-amount${balance === null ? ' loading' : ''}`}>
                {balance === null ? '...' : balance}
              </span>
              <button className="btn-refresh" onClick={() => fetchBalance(address)}>↻</button>
            </div>
          </div>
          <button className="btn-disconnect" onClick={() => setStatus('disconnected')}>
            Disconnect
          </button>
        </div>
      )}
    </div>
  );
}
/* react/B-002.css */
.wallet-demo {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-height: 220px; 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: 280px;
}
.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; color: #fff; }
.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; }

.balance-section { border-top: 1px solid #2a2a3a; padding-top: 14px; display: flex; flex-direction: column; gap: 6px; }
.balance-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: #888; font-weight: 600; }
.balance-fetch-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.balance-amount { font-size: 22px; font-weight: 700; color: #fff; font-variant-numeric: tabular-nums; }
.balance-amount.loading { color: #888; font-size: 16px; }
.btn-refresh {
  padding: 5px 10px; background: transparent; border: 1px solid #2a2a3a;
  border-radius: 6px; font-size: 15px; color: #888; cursor: pointer; transition: background 0.15s, color 0.15s;
}
.btn-refresh:hover { background: #2a2a3a; color: #fff; border-color: #f59e0b; }

.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