B-002 Web3 complex

How to create a eth balance display ETH 残高表示の作り方

Fetch and display the connected wallet's ETH balance in real time using ethers.js v6. Supports loading state, refresh button, and network display. ethers.js (v6) で接続済みウォレットの ETH 残高をリアルタイム取得・表示。ローディング状態・リフレッシュ・ネットワーク表示に対応。

ライブデモ Live Demo

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

接続中... Connecting...
0x0000...0000
ETH Balance
...

概要・用途・特徴Overview, Usage & Features

何ができるかWhat it does

Fetch and display the connected wallet's ETH balance in real time using ethers.js v6. Supports loading state, refresh button, and network display.

ethers.js (v6) で接続済みウォレットの ETH 残高をリアルタイム取得・表示。ローディング状態・リフレッシュ・ネットワーク表示に対応。

どこで使うかWhere to use

dApp interface, wallet integration, blockchain dashboard

ウォレットダッシュボード、DeFiポートフォリオ、アカウント設定ページ、取引確認画面

特徴Key features

Displays live ETH balance fetched from the blockchain via ethers.js or web3.js. Auto-refreshes on block events. Formats balance to human-readable ETH with configurable decimal places. Shows USD equivalent via price API.

ethers.jsまたはweb3.jsを介してブロックチェーンからフェッチしたライブETH残高を表示。ブロックイベントで自動更新。設定可能な小数点以下桁数で人間が読めるETHにフォーマット。価格APIを介してUSD相当額を表示。

調整可能パラメータ Adjustable Parameters

Parameter Default Description
toFixed(4)Decimal places for the balance. Change to <code>toFixed(6)</code> for 6 digits or <code>toFixed(2)</code> for 2 digits
CHAIN_NAMESMapping of chain ID (hex) to network name. Add or remove supported chains freely
ethers.js バージョンFor ethers.js v5 use <code>new ethers.providers.Web3Provider(window.ethereum)</code> / <code>ethers.utils.formatEther()</code> instead
address truncationAdjust <code>slice(0, 6) + '...' + slice(-4)</code> to show more or fewer characters

実装コード Implementation Code

// 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; }
import { useState, useEffect, useCallback } from 'react';
import { BrowserProvider, formatEther } from 'ethers';
import './B-002.css';

const DC_KEY = 'devsnips-wallet-dc'; // localStorage flag: user explicitly disconnected

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

export default function EthBalanceDisplay() {
  // 'init' | 'no-wallet' | 'disconnected' | 'connecting' | 'connected'
  const [status, setStatus] = useState('init');
  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');

    // Silent check — skip if user explicitly disconnected
    window.ethereum.request({ method: 'eth_accounts' })
      .then(accs => { if (accs.length && !localStorage.getItem(DC_KEY)) showConnected(accs[0]); })
      .catch(() => {});

    const onAccounts = (accs) => {
      if (accs.length && !localStorage.getItem(DC_KEY)) {
        showConnected(accs[0]);
      } else if (!accs.length) {
        localStorage.setItem(DC_KEY, '1');
        setStatus('disconnected');
      }
    };
    const onChain = () => {
      if (localStorage.getItem(DC_KEY)) return;
      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');
      // wallet_requestPermissions forces MetaMask to show account picker every time
      await window.ethereum.request({
        method: 'wallet_requestPermissions',
        params: [{ eth_accounts: {} }],
      });
      const [addr] = await window.ethereum.request({ method: 'eth_accounts' });
      localStorage.removeItem(DC_KEY);
      await showConnected(addr);
    } catch {
      setStatus('disconnected'); // user rejected
    }
  }

  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={() => { localStorage.setItem(DC_KEY, '1'); setStatus('disconnected'); }}>
            Disconnect
          </button>
        </div>
      )}
    </div>
  );
}
.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; letter-spacing: 0.02em; }
.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, border-color 0.15s;
  flex-shrink: 0;
  line-height: 1;
}
.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;
  align-self: flex-start;
}
.btn-disconnect:hover { color: #ef4444; }

.no-wallet-msg { font-size: 14px; color: #888; line-height: 1.7; }
.no-wallet-msg a { color: #fbbf24; }

仕組みとカスタマイズHow It Works & Customization

仕組みHow it works

ethers.js provider.getBalance(address) fetches the balance in wei (BigInt). ethers.formatEther() converts to a human-readable ETH string. provider.on("block", ...) triggers a re-fetch on each new block for live updates. A price API (CoinGecko) fetch converts ETH to USD using the current exchange rate.

ethers.js provider.getBalance(address)がweiで残高をフェッチ(BigInt)。ethers.formatEther()が人間が読めるETH文字列に変換。provider.on("block", ...)が各新しいブロックで再フェッチをトリガーしてライブ更新。価格API(CoinGecko)フェッチが現在の為替レートを使用してETHをUSDに変換。

カスタマイズ方法Customization

Add support for ERC-20 token balances by calling the token contract balanceOf. Cache the price API response (30-60s TTL) to reduce API calls. Add a loading skeleton while the balance is fetching.

トークンコントラクトのbalanceOfを呼び出してERC-20トークン残高のサポートを追加。APIコールを減らすために価格APIレスポンスをキャッシュ(30〜60秒TTL)。残高フェッチ中にローディングスケルトンを追加。

注意点Caveats

Never expose private keys or RPC API keys in client-side code. Use rate limiting or caching to avoid hitting free-tier limits on public RPC endpoints. Always handle the case where the provider is unavailable or the user is on the wrong network.

クライアントサイドコードに秘密鍵やRPC APIキーを公開しないでください。パブリックRPCエンドポイントの無料ティアの制限に達しないようレート制限またはキャッシュを使用してください。プロバイダーが利用できないまたはユーザーが間違ったネットワークにいる場合を常に処理してください。

よくある質問 Frequently Asked Questions

How to customize the eth balance display? ETH Balance Displayをカスタマイズするには?

Modify the CSS custom properties and class styles defined in the code section. Key adjustable values include colors, sizes, durations, and spacing. See the Adjustable Parameters section for specific variables.

コードセクションで定義されているCSSカスタムプロパティとクラススタイルを変更してください。色、サイズ、時間、間隔が主な調整可能値です。具体的な変数は調整可能パラメータセクションを参照してください。

How to use the eth balance display in React? ReactでETH Balance Displayを使うには?

Import the provided React component and its CSS file. The component accepts props for customization. Check the React code section for the full implementation and available props.

提供されているReactコンポーネントとCSSファイルをインポートしてください。コンポーネントのpropsでカスタマイズできます。完全な実装と利用可能なpropsはReactコードセクションを参照してください。

What are the performance implications of eth balance display? ETH Balance Displayのパフォーマンスへの影響は?

This implementation uses CSS transforms and opacity for animations, which are GPU-accelerated. It's lightweight and doesn't cause layout thrashing. Consider using prefers-reduced-motion for accessibility.

この実装はCSSのtransformとopacityを使用しており、GPUアクセラレーションされます。軽量でレイアウトスラッシングを引き起こしません。アクセシビリティのためにprefers-reduced-motionの使用を検討してください。

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

以下をAIに貼り付けると実装を依頼できます。 Paste the following into your AI assistant to request implementation.