ライブデモ Live Demo
MetaMask(または EIP-1193 対応ウォレット)が必要です。接続後、ethers.js v6 の BrowserProvider で ETH 残高をリアルタイム取得します。
Requires MetaMask or an EIP-1193 wallet. After connecting, ETH balance is fetched in real time using ethers.js v6 BrowserProvider.
AI向け説明 AI Description
B-002 は ethers.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
-
toFixed(4) —
残高の小数点以下桁数。
toFixed(6)で6桁、toFixed(2)で2桁に変更可能 Decimal places for the balance. Change totoFixed(6)for 6 digits ortoFixed(2)for 2 digits - CHAIN_NAMES — チェーン ID(16進数)→ ネットワーク名のマッピング。対応チェーンを自由に追加・削除可能 Mapping of chain ID (hex) to network name. Add or remove supported chains freely
-
ethers.js バージョン —
v5 を使う場合は
new ethers.providers.Web3Provider(window.ethereum)/ethers.utils.formatEther()に変更する For ethers.js v5 usenew ethers.providers.Web3Provider(window.ethereum)/ethers.utils.formatEther()instead -
address truncation —
slice(0, 6) + '...' + slice(-4)の数値を変更して表示量を調整 Adjustslice(0, 6) + '...' + slice(-4)to show more or fewer characters
実装 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
-
ethers.js v5 との API 差異: v5 では
new ethers.providers.Web3Provider(window.ethereum)/ethers.utils.formatEther()を使います。v6 ではBrowserProvider/formatEther(トップレベル関数)に変わっています。 ethers.js v5 API differences: In v5 usenew ethers.providers.Web3Provider(window.ethereum)andethers.utils.formatEther(). In v6 these areBrowserProviderand top-levelformatEther(). -
ERC-20 トークン残高の取得:
new ethers.Contract(tokenAddress, ['function balanceOf(address) view returns (uint256)'], provider)を作りcontract.balanceOf(address)で取得。decimals()で単位を合わせる。 Fetching ERC-20 token balance: Createnew ethers.Contract(tokenAddress, ['function balanceOf(address) view returns (uint256)'], provider)and callcontract.balanceOf(address). Usedecimals()to normalize the unit. -
USD 換算表示: CoinGecko の
/simple/price?ids=ethereum&vs_currencies=usdで価格を取得し、残高に掛け合わせると USD 換算額を表示できます。 USD conversion: Fetch the ETH price from CoinGecko's/simple/price?ids=ethereum&vs_currencies=usdendpoint and multiply by the balance to display the USD equivalent. -
切断の仕組み: MetaMask にプログラム的な切断 API はありません。Disconnect 時に
localStorageフラグをセットしてページリロード後の自動接続を防ぎます。再接続時はwallet_requestPermissionsでアカウント選択ポップアップを強制表示します。 How disconnect works: MetaMask has no programmatic disconnect API. AlocalStorageflag prevents silent auto-reconnect on page reload. On reconnect,wallet_requestPermissionsforces the account picker popup.