ライブデモ Live Demo
MetaMask(または EIP-1193 対応ウォレット)が必要です。接続・ネットワーク表示・アドレスコピー・切断をデモします。
Requires MetaMask or an EIP-1193 wallet. Demonstrates connect, network display, address copy, and disconnect.
AI向け説明 AI Description
B-001 は window.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
- CHAIN_NAMES — チェーン ID(16進数)とネットワーク名のマッピング。対応チェーンを追加・削除可能 Mapping of chain ID (hex) to network name. Add or remove supported chains freely
-
address truncation —
slice(0, 6) + '...' + slice(-4)で先頭6字・末尾4字を表示。数値を変更して表示量を調整slice(0, 6) + '...' + slice(-4)shows first 6 and last 4 chars. Adjust numbers to show more or fewer characters - eth_requestAccounts → eth_accounts — 承認ダイアログを出さずに既接続アカウントを取得する(ページロード時の再接続チェックに使用) Retrieves already-connected accounts without prompting the user (used for reconnect check on page load)
-
button style —
.btn-connectの背景グラデーション・角丸・シャドウで見た目をカスタマイズ Customize the look via.btn-connectgradient, border-radius, and box-shadow
実装 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
-
切断について: MetaMask は
rpc_disconnectなどの「プログラム的な切断API」を提供しません。btn-disconnectはあくまで UI の状態リセットです。実際の切断はユーザーがウォレット側で行います。 Disconnect caveat: MetaMask does not provide a programmatic disconnect API.btn-disconnectonly resets the UI state. Actual disconnection must be done from the wallet side by the user. -
互換性:
window.ethereumは MetaMask だけでなく、Rabby・Coinbase Wallet・Brave Wallet など EIP-1193 準拠のウォレットでも動作します。 Compatibility:window.ethereumworks with any EIP-1193 compliant wallet — MetaMask, Rabby, Coinbase Wallet, Brave Wallet, etc. -
チェーン変更対応: 本番では
chainChangedで対応チェーン外に変更された場合、「このネットワークは未対応です」と警告を表示するのが推奨です。 Chain validation: In production, show an "unsupported network" warning whenchainChangedfires with a chain outside your supported list. -
α 構成 (B-002+): ethers.js CDN を追加すると
provider.getBalance(address)でETH残高取得、signer.signMessage()でウォレット署名、new ethers.Contract()でスマートコントラクト呼び出しが可能になります。 α builds (B-002+): Add ethers.js via CDN to enableprovider.getBalance(address)for ETH balance,signer.signMessage()for wallet signing, andnew ethers.Contract()for smart contract calls.