簡易ETH錢包
代碼采用React+TypeScript開發(fā),保持了良好的可維護性和用戶體驗:
功能說明
-
BIP-39助記詞功能
- 完全符合BIP-39標準,生成12個單詞的助記詞
- 強制助記詞驗證流程,確保用戶正確備份
- 支持通過助記詞導(dǎo)入錢包,兼容行業(yè)標準
-
ERC-20代幣支持
- 內(nèi)置常用ERC-20代幣列表(USDT、USDC、DAI等)
- 支持添加自定義ERC-20代幣,自動識別代幣名稱、符號和小數(shù)位
- 實時顯示代幣余額,支持代幣轉(zhuǎn)賬功能
-
交易歷史查詢
- 整合Etherscan API查詢ETH和ERC-20代幣交易歷史
- 顯示交易哈希、發(fā)送方、接收方、金額、時間和確認數(shù)
- 支持交易歷史刷新,區(qū)分ETH和代幣交易
-
私鑰AES加密存儲
- 使用crypto-js庫實現(xiàn)AES加密算法
- 私鑰加密后存儲在localStorage,不直接存儲明文
- 錢包解鎖需要密碼驗證,增強安全性
實現(xiàn)代碼
實現(xiàn)代碼
import React, { useState, useEffect } from 'react';
import { ethers, HDNodeWallet } from 'ethers';
import CryptoJS from 'crypto-js';
import axios from 'axios';
// ERC-20代幣標準ABI
const ERC20_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
// 常用ERC-20代幣列表(Sepolia測試網(wǎng))
const COMMON_TOKENS = [
{
symbol: "USDT",
address: "0x7163aF91147b087166083F24Eb3a99F59F451039",
decimals: 18
},
{
symbol: "USDC",
address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
decimals: 6
},
{
symbol: "DAI",
address: "0x8EC166D656226935917538F69D3F660416aD219d",
decimals: 18
}
];
// 樣式組件
const Container = ({ children }: { children: React.ReactNode }) => (
<div style={{
maxWidth: 900,
margin: '0 auto',
padding: '20px',
fontFamily: 'Arial, sans-serif',
backgroundColor: '#f9f9f9',
minHeight: '100vh'
}}>
{children}
</div>
);
const Card = ({ title, children }: { title: string; children: React.ReactNode }) => (
<div style={{
marginBottom: '20px',
padding: '15px',
border: '1px solid #e0e0e0',
borderRadius: '8px',
backgroundColor: 'white',
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
}}>
<h3 style={{
margin: '0 0 15px 0',
color: '#2c3e50',
fontSize: '1.2rem',
borderBottom: '1px solid #f0f0f0',
paddingBottom: '8px'
}}>{title}</h3>
{children}
</div>
);
const Button = ({
onClick,
children,
disabled = false,
primary = false,
danger = false
}: {
onClick: () => void;
children: React.ReactNode;
disabled?: boolean;
primary?: boolean;
danger?: boolean;
}) => (
<button
onClick={onClick}
disabled={disabled}
style={{
padding: '8px 16px',
fontSize: '14px',
cursor: disabled ? 'not-allowed' : 'pointer',
border: 'none',
borderRadius: '4px',
backgroundColor: disabled
? '#f0f0f0'
: danger
? '#e74c3c'
: primary
? '#3498db'
: '#2ecc71',
color: 'white',
margin: '5px',
transition: 'background-color 0.2s',
'&:hover': {
opacity: 0.9
}
}}
>
{children}
</button>
);
const Input = ({
value,
onChange,
placeholder,
type = 'text',
style = {},
multiline = false
}: {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
placeholder: string;
type?: string;
style?: React.CSSProperties;
multiline?: boolean;
}) => {
const Element = multiline ? 'textarea' : 'input';
return React.createElement(Element, {
type,
value,
onChange,
placeholder,
style: {
padding: '10px',
fontSize: '14px',
borderRadius: '4px',
border: '1px solid #ddd',
width: '100%',
boxSizing: 'border-box',
minHeight: multiline ? '80px' : 'auto',
resize: multiline ? 'vertical' : 'none',
...style
}
});
};
const Alert = ({ message, type = 'info' }: { message: string; type?: 'info' | 'error' | 'success' }) => {
const styles = {
info: { bg: '#e3f2fd', text: '#1565c0', border: '#bbdefb' },
error: { bg: '#ffebee', text: '#b71c1c', border: '#f8bbd0' },
success: { bg: '#e8f5e9', text: '#2e7d32', border: '#c8e6c9' }
};
const style = styles[type];
return (
<div style={{
padding: '12px',
borderRadius: '4px',
backgroundColor: style.bg,
color: style.text,
border: `1px solid ${style.border}`,
margin: '10px 0',
fontSize: '14px'
}}>
{message}
</div>
);
};
const Modal = ({
visible,
onClose,
title,
children,
footer
}: {
visible: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
footer?: React.ReactNode;
}) => {
if (!visible) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
width: '90%',
maxWidth: 600,
maxHeight: '80vh',
overflowY: 'auto'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, color: '#2c3e50' }}>{title}</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '20px',
color: '#777'
}}
>
×
</button>
</div>
{children}
{footer && (
<div style={{ marginTop: '20px', display: 'flex', justifyContent: 'flex-end' }}>
{footer}
</div>
)}
</div>
</div>
);
};
// 主錢包組件
const Wallet: React.FC = () => {
// 核心狀態(tài)管理
const [address, setAddress] = useState<string>('');
const [privateKey, setPrivateKey] = useState<string>('');
const [mnemonic, setMnemonic] = useState<string>('');
const [ethBalance, setEthBalance] = useState<string>('0');
const [recipient, setRecipient] = useState<string>('');
const [amount, setAmount] = useState<string>('0.01');
const [loading, setLoading] = useState<boolean>(false);
const [message, setMessage] = useState<{ text: string; type: 'info' | 'error' | 'success' } | null>(null);
const [provider, setProvider] = useState<ethers.JsonRpcProvider | null>(null);
const [showPrivateKey, setShowPrivateKey] = useState<boolean>(false);
const [importType, setImportType] = useState<'privateKey' | 'mnemonic'>('privateKey');
const [importValue, setImportValue] = useState<string>('');
// 加密相關(guān)狀態(tài)
const [password, setPassword] = useState<string>('');
const [confirmPassword, setConfirmPassword] = useState<string>('');
const [unlockPassword, setUnlockPassword] = useState<string>('');
const [isLocked, setIsLocked] = useState<boolean>(true);
// 代幣相關(guān)狀態(tài)
const [tokens, setTokens] = useState<Array<{
symbol: string;
address: string;
balance: string;
decimals: number;
name: string;
}>>([]);
const [selectedToken, setSelectedToken] = useState<string>('ETH');
const [tokenAddress, setTokenAddress] = useState<string>('');
// 交易歷史相關(guān)狀態(tài)
const [transactions, setTransactions] = useState<Array<{
hash: string;
from: string;
to: string;
value: string;
tokenSymbol?: string;
timeStamp: string;
confirmations: string;
}>>([]);
const [transactionsLoading, setTransactionsLoading] = useState<boolean>(false);
// 彈窗狀態(tài)
const [mnemonicModalVisible, setMnemonicModalVisible] = useState<boolean>(false);
const [saveReminderVisible, setSaveReminderVisible] = useState<boolean>(false);
const [confirmMnemonic, setConfirmMnemonic] = useState<string>('');
const [mnemonicConfirmed, setMnemonicConfirmed] = useState<boolean>(false);
const [addTokenModalVisible, setAddTokenModalVisible] = useState<boolean>(false);
const [transactionHistoryVisible, setTransactionHistoryVisible] = useState<boolean>(false);
// 初始化Provider和檢查存儲的錢包
useEffect(() => {
// 初始化以太坊測試網(wǎng)Provider
const initProvider = () => {
try {
const newProvider = new ethers.JsonRpcProvider(
'https://sepolia.infura.io/v3/your-api-key' // 替換為你的Infura API密鑰
);
setProvider(newProvider);
} catch (err) {
setMessage({ text: '初始化區(qū)塊鏈連接失敗', type: 'error' });
console.error(err);
}
};
// 檢查是否有加密存儲的私鑰
const checkStoredWallet = () => {
const encryptedKey = localStorage.getItem('encrypted_eth_private_key');
if (encryptedKey) {
setIsLocked(true); // 需要密碼解鎖
} else {
setIsLocked(false); // 沒有存儲的錢包,顯示創(chuàng)建/導(dǎo)入界面
}
};
initProvider();
checkStoredWallet();
}, []);
// 錢包解鎖后加載數(shù)據(jù)
useEffect(() => {
if (address && provider && !isLocked) {
loadTokens();
fetchTransactionHistory();
}
}, [address, provider, isLocked]);
// 解鎖錢包(AES解密)
const unlockWallet = () => {
if (!unlockPassword) {
setMessage({ text: '請輸入密碼', type: 'error' });
return;
}
try {
const encryptedKey = localStorage.getItem('encrypted_eth_private_key');
if (!encryptedKey) {
setMessage({ text: '沒有找到存儲的錢包', type: 'error' });
return;
}
// 使用AES解密私鑰
const bytes = CryptoJS.AES.decrypt(encryptedKey, unlockPassword);
const privateKey = bytes.toString(CryptoJS.enc.Utf8);
if (!privateKey || !privateKey.startsWith('0x')) {
setMessage({ text: '密碼錯誤', type: 'error' });
return;
}
const wallet = new ethers.Wallet(privateKey);
setPrivateKey(privateKey);
setAddress(wallet.address);
setIsLocked(false);
setMessage({ text: '錢包解鎖成功', type: 'success' });
} catch (err) {
setMessage({ text: '解鎖失敗,密碼可能不正確', type: 'error' });
console.error(err);
}
};
// 創(chuàng)建新錢包(BIP-39標準)
const createWallet = async () => {
if (!password) {
setMessage({ text: '請設(shè)置密碼', type: 'error' });
return;
}
if (password !== confirmPassword) {
setMessage({ text: '兩次輸入的密碼不一致', type: 'error' });
return;
}
try {
// 生成符合BIP-39標準的隨機錢包
const wallet = ethers.Wallet.createRandom();
const mnemonicPhrase = wallet.mnemonic.phrase;
setPrivateKey(wallet.privateKey);
setAddress(wallet.address);
setMnemonic(mnemonicPhrase);
setConfirmMnemonic('');
setMnemonicConfirmed(false);
// 顯示助記詞彈窗
setMnemonicModalVisible(true);
} catch (err) {
setMessage({
text: `創(chuàng)建錢包失敗: ${err instanceof Error ? err.message : String(err)}`,
type: 'error'
});
}
};
// 驗證助記詞并加密保存私鑰
const verifyMnemonic = () => {
if (!confirmMnemonic.trim()) {
setMessage({ text: '請輸入助記詞', type: 'error' });
return;
}
if (confirmMnemonic.trim() === mnemonic.trim()) {
// 驗證成功,使用AES加密私鑰并存儲
const encrypted = CryptoJS.AES.encrypt(privateKey, password).toString();
localStorage.setItem('encrypted_eth_private_key', encrypted);
setMnemonicConfirmed(true);
setIsLocked(false);
setMessage({ text: '助記詞驗證成功,錢包已創(chuàng)建', type: 'success' });
setSaveReminderVisible(true);
} else {
setMessage({ text: '助記詞不匹配,請重新輸入', type: 'error' });
}
};
// 導(dǎo)入錢包(私鑰或助記詞)
const importWallet = () => {
if (!importValue.trim()) {
setMessage({ text: '請輸入私鑰或助記詞', type: 'error' });
return;
}
if (!password) {
setMessage({ text: '請設(shè)置密碼', type: 'error' });
return;
}
try {
let wallet: ethers.Wallet;
if (importType === 'privateKey') {
// 私鑰導(dǎo)入
if (!importValue.startsWith('0x')) {
setMessage({ text: '私鑰必須以0x開頭', type: 'error' });
return;
}
wallet = new ethers.Wallet(importValue);
} else {
// 助記詞導(dǎo)入 (BIP-39標準)
const words = importValue.trim().split(/\s+/);
// BIP-39支持12, 15, 18, 21或24個單詞
if (![12, 15, 18, 21, 24].includes(words.length)) {
setMessage({ text: '助記詞必須是12, 15, 18, 21或24個單詞', type: 'error' });
return;
}
wallet = HDNodeWallet.fromPhrase(importValue);
}
// 加密并保存私鑰
const encrypted = CryptoJS.AES.encrypt(wallet.privateKey, password).toString();
localStorage.setItem('encrypted_eth_private_key', encrypted);
setPrivateKey(wallet.privateKey);
setAddress(wallet.address);
setIsLocked(false);
setMessage({ text: '錢包導(dǎo)入成功', type: 'success' });
setImportValue('');
setPassword('');
} catch (err) {
setMessage({
text: `導(dǎo)入失敗: ${err instanceof Error ? err.message : String(err)}`,
type: 'error'
});
}
};
// 加載ERC-20代幣余額
const loadTokens = async () => {
if (!address || !provider) return;
try {
setLoading(true);
// 先加載ETH余額
const balance = await provider.getBalance(address);
setEthBalance(ethers.formatEther(balance));
// 加載常用代幣
const tokenData = await Promise.all(COMMON_TOKENS.map(async (token) => {
try {
const contract = new ethers.Contract(token.address, ERC20_ABI, provider);
const balance = await contract.balanceOf(address);
const name = await contract.name();
const symbol = await contract.symbol();
const decimals = await contract.decimals();
return {
symbol,
address: token.address,
balance: ethers.formatUnits(balance, decimals),
decimals,
name
};
} catch (err) {
console.error(`加載${token.symbol}失敗:`, err);
return null;
}
}));
// 過濾掉加載失敗的代幣
const validTokens = tokenData.filter(Boolean) as Array<{
symbol: string;
address: string;
balance: string;
decimals: number;
name: string;
}>;
// 加載用戶添加的自定義代幣
const customTokens = JSON.parse(localStorage.getItem('custom_erc20_tokens') || '[]');
if (customTokens.length > 0) {
const customTokenData = await Promise.all(customTokens.map(async (token: any) => {
try {
const contract = new ethers.Contract(token.address, ERC20_ABI, provider);
const balance = await contract.balanceOf(address);
const name = await contract.name();
const symbol = await contract.symbol();
const decimals = await contract.decimals();
return {
symbol,
address: token.address,
balance: ethers.formatUnits(balance, decimals),
decimals,
name
};
} catch (err) {
console.error(`加載自定義代幣失敗:`, err);
return null;
}
}));
validTokens.push(...customTokenData.filter(Boolean) as any);
}
setTokens(validTokens);
} catch (err) {
setMessage({ text: '加載代幣失敗', type: 'error' });
console.error(err);
} finally {
setLoading(false);
}
};
// 添加自定義ERC-20代幣
const addCustomToken = async () => {
if (!tokenAddress || !ethers.isAddress(tokenAddress)) {
setMessage({ text: '請輸入有效的代幣合約地址', type: 'error' });
return;
}
if (!provider) return;
try {
setLoading(true);
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
// 驗證ERC-20合約
const name = await contract.name();
const symbol = await contract.symbol();
const decimals = await contract.decimals();
// 檢查余額
const balance = await contract.balanceOf(address);
// 保存到本地存儲
const customTokens = JSON.parse(localStorage.getItem('custom_erc20_tokens') || '[]');
// 避免重復(fù)添加
if (!customTokens.some((t: any) => t.address.toLowerCase() === tokenAddress.toLowerCase())) {
customTokens.push({ address: tokenAddress });
localStorage.setItem('custom_erc20_tokens', JSON.stringify(customTokens));
}
// 更新代幣列表
setTokens([...tokens, {
symbol,
address: tokenAddress,
balance: ethers.formatUnits(balance, decimals),
decimals,
name
}]);
setTokenAddress('');
setAddTokenModalVisible(false);
setMessage({ text: `成功添加代幣: ${symbol}`, type: 'success' });
} catch (err) {
setMessage({ text: '添加代幣失敗,可能不是有效的ERC-20合約', type: 'error' });
console.error(err);
} finally {
setLoading(false);
}
};
// 查詢交易歷史
const fetchTransactionHistory = async () => {
if (!address) return;
try {
setTransactionsLoading(true);
// 使用Etherscan API查詢交易歷史
const apiKey = 'your-etherscan-api-key'; // 替換為你的Etherscan API密鑰
const chainId = (await provider?.getNetwork())?.chainId || 11155111; // Sepolia測試網(wǎng)
// 構(gòu)建API URL
let baseUrl = chainId === 1
? 'https://api.etherscan.io/api'
: 'https://api-sepolia.etherscan.io/api';
// 查詢ETH交易
const ethTxUrl = new URL(baseUrl);
ethTxUrl.searchParams.append('module', 'account');
ethTxUrl.searchParams.append('action', 'txlist');
ethTxUrl.searchParams.append('address', address);
ethTxUrl.searchParams.append('startblock', '0');
ethTxUrl.searchParams.append('endblock', '99999999');
ethTxUrl.searchParams.append('page', '1');
ethTxUrl.searchParams.append('offset', '20');
ethTxUrl.searchParams.append('sort', 'desc');
ethTxUrl.searchParams.append('apikey', apiKey);
const ethTxResponse = await axios.get(ethTxUrl.toString());
if (ethTxResponse.data.status !== '1') {
setMessage({ text: '查詢ETH交易歷史失敗', type: 'error' });
setTransactionsLoading(false);
return;
}
// 格式化ETH交易
const ethTransactions = ethTxResponse.data.result.map((tx: any) => ({
hash: tx.hash,
from: tx.from,
to: tx.to || '合約創(chuàng)建',
value: ethers.formatEther(tx.value),
timeStamp: new Date(parseInt(tx.timeStamp) * 1000).toLocaleString(),
confirmations: tx.confirmations
}));
// 查詢ERC-20代幣交易
const tokenTxUrl = new URL(baseUrl);
tokenTxUrl.searchParams.append('module', 'account');
tokenTxUrl.searchParams.append('action', 'tokentx');
tokenTxUrl.searchParams.append('address', address);
tokenTxUrl.searchParams.append('startblock', '0');
tokenTxUrl.searchParams.append('endblock', '99999999');
tokenTxUrl.searchParams.append('page', '1');
tokenTxUrl.searchParams.append('offset', '20');
tokenTxUrl.searchParams.append('sort', 'desc');
tokenTxUrl.searchParams.append('apikey', apiKey);
const tokenTxResponse = await axios.get(tokenTxUrl.toString());
// 格式化代幣交易
let tokenTransactions: any[] = [];
if (tokenTxResponse.data.status === '1') {
tokenTransactions = tokenTxResponse.data.result.map((tx: any) => {
// 查找對應(yīng)的代幣信息
const token = tokens.find(t => t.address.toLowerCase() === tx.contractAddress.toLowerCase());
const decimals = token?.decimals || 18;
return {
hash: tx.hash,
from: tx.from,
to: tx.to,
value: ethers.formatUnits(tx.value, decimals),
tokenSymbol: tx.tokenSymbol,
timeStamp: new Date(parseInt(tx.timeStamp) * 1000).toLocaleString(),
confirmations: tx.confirmations
};
});
}
// 合并并按時間排序所有交易
const allTransactions = [...ethTransactions, ...tokenTransactions]
.sort((a, b) => new Date(b.timeStamp).getTime() - new Date(a.timeStamp).getTime());
setTransactions(allTransactions);
} catch (err) {
setMessage({ text: '查詢交易歷史失敗', type: 'error' });
console.error(err);
} finally {
setTransactionsLoading(false);
}
};
// 發(fā)送交易 (ETH或ERC-20代幣)
const sendTransaction = async () => {
if (!privateKey || !recipient || !amount || !provider || isLocked) {
setMessage({ text: '請?zhí)顚懲暾畔⒒蚪怄i錢包', type: 'error' });
return;
}
// 地址校驗
if (!ethers.isAddress(recipient)) {
setMessage({ text: '無效的接收地址', type: 'error' });
return;
}
// 金額校驗
const numAmount = parseFloat(amount);
if (isNaN(numAmount) || numAmount <= 0 || numAmount > 10000) {
setMessage({ text: '請輸入有效的金額(正數(shù)且不超過10000)', type: 'error' });
return;
}
setLoading(true);
try {
const wallet = new ethers.Wallet(privateKey, provider);
if (selectedToken === 'ETH') {
// 發(fā)送ETH
// 余額檢查
if (numAmount > parseFloat(ethBalance)) {
setMessage({ text: 'ETH余額不足', type: 'error' });
setLoading(false);
return;
}
const tx = {
to: recipient,
value: ethers.parseEther(amount),
gasLimit: 21000
};
const txResponse = await wallet.sendTransaction(tx);
setMessage({ text: `ETH交易已發(fā)送,哈希: ${txResponse.hash}`, type: 'success' });
} else {
// 發(fā)送ERC-20代幣
const token = tokens.find(t => t.symbol === selectedToken);
if (!token) {
setMessage({ text: '未找到選中的代幣', type: 'error' });
setLoading(false);
return;
}
// 余額檢查
if (numAmount > parseFloat(token.balance)) {
setMessage({ text: `${token.symbol}余額不足`, type: 'error' });
setLoading(false);
return;
}
const contract = new ethers.Contract(token.address, ERC20_ABI, wallet);
const amountWei = ethers.parseUnits(amount, token.decimals);
const txResponse = await contract.transfer(recipient, amountWei);
setMessage({ text: `${token.symbol}交易已發(fā)送,哈希: ${txResponse.hash}`, type: 'success' });
}
// 重置表單
setRecipient('');
setAmount('0.01');
// 延遲刷新數(shù)據(jù),等待鏈上確認
setTimeout(() => {
loadTokens();
fetchTransactionHistory();
}, 10000);
} catch (err) {
setMessage({
text: `交易失敗: ${err instanceof Error ? err.message : String(err)}`,
type: 'error'
});
console.error(err);
} finally {
setLoading(false);
}
};
// 渲染錢包鎖定狀態(tài)
if (isLocked && localStorage.getItem('encrypted_eth_private_key')) {
return (
<Container>
<h2 style={{ color: '#2c3e50', textAlign: 'center' }}>ETH錢包</h2>
<Card title="解鎖錢包">
<p>請輸入密碼解鎖你的錢包</p>
<Input
type="password"
value={unlockPassword}
onChange={(e) => setUnlockPassword(e.target.value)}
placeholder="錢包密碼"
/>
<div style={{ marginTop: '15px' }}>
<Button onClick={unlockWallet} primary>
解鎖錢包
</Button>
</div>
</Card>
{message && <Alert message={message.text} type={message.type} />}
</Container>
);
}
// 渲染主界面
return (
<Container>
<h2 style={{ color: '#2c3e50', textAlign: 'center' }}>ETH錢包</h2>
{/* 錢包未創(chuàng)建時顯示創(chuàng)建/導(dǎo)入選項 */}
{!address && !isLocked && (
<>
{/* 創(chuàng)建錢包 */}
<Card title="創(chuàng)建新錢包">
<p>創(chuàng)建新錢包將生成符合BIP-39標準的12個單詞助記詞,請務(wù)必妥善保管。</p>
<div style={{ marginBottom: '10px' }}>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="設(shè)置錢包密碼(用于加密私鑰)"
/>
</div>
<div style={{ marginBottom: '10px' }}>
<Input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="確認密碼"
/>
</div>
<Button onClick={createWallet} primary>
創(chuàng)建新錢包
</Button>
<Alert message="密碼用于加密存儲私鑰,請牢記你的密碼和助記詞!" type="info" />
</Card>
{/* 導(dǎo)入錢包 */}
<Card title="導(dǎo)入已有錢包">
<div style={{ marginBottom: '10px' }}>
<label style={{ marginRight: '15px' }}>
<input
type="radio"
checked={importType === 'privateKey'}
onChange={() => setImportType('privateKey')}
/>
私鑰導(dǎo)入
</label>
<label>
<input
type="radio"
checked={importType === 'mnemonic'}
onChange={() => setImportType('mnemonic')}
/>
助記詞導(dǎo)入 (BIP-39)
</label>
</div>
<div style={{ marginBottom: '10px' }}>
<Input
value={importValue}
onChange={(e) => setImportValue(e.target.value)}
placeholder={importType === 'privateKey'
? '輸入私鑰(以0x開頭)'
: '輸入12, 15, 18, 21或24個單詞的助記詞,空格分隔'
}
multiline={importType === 'mnemonic'}
/>
</div>
<div style={{ marginBottom: '10px' }}>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="設(shè)置錢包密碼(用于加密私鑰)"
/>
</div>
<Button onClick={importWallet} primary>
導(dǎo)入錢包
</Button>
</Card>
</>
)}
{/* 錢包已創(chuàng)建時顯示賬戶信息 */}
{address && !isLocked && (
<>
{/* 賬戶信息 */}
<Card title="賬戶信息">
<div style={{ marginBottom: '10px', wordBreak: 'break-all' }}>
<strong>地址:</strong> {address}
</div>
<div style={{ marginBottom: '15px', wordBreak: 'break-all' }}>
<strong>私鑰:</strong>
{showPrivateKey ? privateKey : '********************'}
<Button
onClick={() => setShowPrivateKey(!showPrivateKey)}
style={{ padding: '2px 8px', fontSize: '12px', marginLeft: '10px' }}
>
{showPrivateKey ? '隱藏' : '顯示'}
</Button>
</div>
<div>
<Button onClick={loadTokens} disabled={loading}>
{loading ? '刷新中...' : '刷新資產(chǎn)'}
</Button>
<Button onClick={() => setTransactionHistoryVisible(true)}>
查看交易歷史
</Button>
</div>
</Card>
{/* 資產(chǎn)列表 */}
<Card title="資產(chǎn)列表">
<div style={{ marginBottom: '10px', padding: '10px', backgroundColor: '#f9f9f9', borderRadius: '4px' }}>
<strong>ETH</strong>: {ethBalance}
</div>
{tokens.length > 0 ? (
tokens.map((token) => (
<div
key={token.address}
style={{
marginBottom: '10px',
padding: '10px',
backgroundColor: '#f9f9f9',
borderRadius: '4px'
}}
>
<strong>{token.symbol}</strong> ({token.name}): {token.balance}
</div>
))
) : (
<Alert message="沒有檢測到ERC-20代幣" type="info" />
)}
<Button onClick={() => setAddTokenModalVisible(true)}>
添加ERC-20代幣
</Button>
</Card>
{/* 發(fā)送交易 */}
<Card title="發(fā)送資產(chǎn)">
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>選擇資產(chǎn):</label>
<select
value={selectedToken}
onChange={(e) => setSelectedToken(e.target.value)}
style={{
padding: '10px',
width: '100%',
borderRadius: '4px',
border: '1px solid #ddd',
fontSize: '14px'
}}
>
<option value="ETH">ETH</option>
{tokens.map((token) => (
<option key={token.address} value={token.symbol}>
{token.symbol} ({token.name})
</option>
))}
</select>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>接收地址:</label>
<Input
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="0x..."
/>
{recipient && !ethers.isAddress(recipient) && (
<Alert message="地址格式無效" type="error" />
)}
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>
金額 ({selectedToken}):
</label>
<Input
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.01"
/>
{amount && isNaN(parseFloat(amount)) && (
<Alert message="請輸入有效的金額" type="error" />
)}
</div>
<Button onClick={sendTransaction} disabled={loading} primary>
{loading ? '處理中...' : `發(fā)送 ${selectedToken}`}
</Button>
</Card>
</>
)}
{/* 消息提示 */}
{message && <Alert message={message.text} type={message.type} />}
{/* 助記詞彈窗 */}
<Modal
visible={mnemonicModalVisible}
onClose={() => !mnemonicConfirmed && window.confirm('助記詞未備份,確定要關(guān)閉嗎?') && setMnemonicModalVisible(false)}
title="你的助記詞 - 請務(wù)必備份!"
footer={
mnemonicConfirmed ? (
<Button onClick={() => setMnemonicModalVisible(false)}>完成</Button>
) : (
<Button onClick={verifyMnemonic} primary>驗證助記詞</Button>
)
}
>
<Alert message="這是你錢包的唯一備份,請在安全的地方離線記錄下來!切勿截圖或在網(wǎng)上分享!" type="error" />
<div style={{
backgroundColor: '#f5f5f5',
padding: '15px',
borderRadius: '4px',
margin: '15px 0',
wordBreak: 'break-all',
lineHeight: '1.6'
}}>
{mnemonic.split(' ').map((word, i) => (
<span key={i} style={{
display: 'inline-block',
width: '80px',
padding: '5px',
margin: '2px',
backgroundColor: '#eee',
borderRadius: '2px',
textAlign: 'center'
}}>
{i+1}. {word}
</span>
))}
</div>
{!mnemonicConfirmed && (
<>
<p>請重新輸入上面的助記詞以確認備份:</p>
<Input
value={confirmMnemonic}
onChange={(e) => setConfirmMnemonic(e.target.value)}
placeholder="輸入完整的助記詞,用空格分隔"
multiline
/>
</>
)}
</Modal>
{/* 保存提醒彈窗 */}
<Modal
visible={saveReminderVisible}
onClose={() => setSaveReminderVisible(false)}
title="重要提醒"
footer={
<Button onClick={() => setSaveReminderVisible(false)} primary>
我已了解并保存
</Button>
}
>
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}>??</div>
<h4 style={{ color: '#b71c1c', marginBottom: '15px' }}>請務(wù)必牢記你的助記詞!</h4>
<p style={{ marginBottom: '10px', textAlign: 'left' }}>1. 助記詞是恢復(fù)錢包的唯一方式</p>
<p style={{ marginBottom: '10px', textAlign: 'left' }}>2. 丟失助記詞將導(dǎo)致資產(chǎn)永久丟失</p>
<p style={{ marginBottom: '10px', textAlign: 'left' }}>3. 不要向任何人透露你的助記詞</p>
<p style={{ textAlign: 'left' }}>4. 建議手寫備份并保存在安全的地方</p>
</div>
</Modal>
{/* 添加代幣彈窗 */}
<Modal
visible={addTokenModalVisible}
onClose={() => setAddTokenModalVisible(false)}
title="添加ERC-20代幣"
footer={
<Button onClick={addCustomToken} primary>
添加代幣
</Button>
}
>
<p>請輸入ERC-20代幣合約地址:</p>
<Input
value={tokenAddress}
onChange={(e) => setTokenAddress(e.target.value)}
placeholder="0x..."
/>
<Alert message="請確保合約地址正確,添加錯誤的合約可能導(dǎo)致資產(chǎn)顯示異常" type="info" />
<p style={{ fontSize: '12px', color: '#777' }}>
提示:合約地址通常可以在代幣的官方網(wǎng)站或區(qū)塊瀏覽器上找到
</p>
</Modal>
{/* 交易歷史彈窗 */}
<Modal
visible={transactionHistoryVisible}
onClose={() => setTransactionHistoryVisible(false)}
title="交易歷史"
footer={
<Button onClick={fetchTransactionHistory} disabled={transactionsLoading}>
{transactionsLoading ? '刷新中...' : '刷新'}
</Button>
}
>
{transactionsLoading ? (
<div style={{ textAlign: 'center', padding: '20px' }}>加載交易歷史中...</div>
) : transactions.length === 0 ? (
<div style={{ textAlign: 'center', padding: '20px' }}>沒有找到交易歷史</div>
) : (
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
{transactions.map((tx, index) => (
<div key={index} style={{
padding: '12px',
borderBottom: '1px solid #eee',
marginBottom: '10px'
}}>
<div style={{ marginBottom: '5px', fontSize: '13px' }}>
<strong>哈希:</strong> {tx.hash.substring(0, 10)}...{tx.hash.substring(tx.hash.length - 10)}
</div>
<div style={{ marginBottom: '5px', fontSize: '13px' }}>
<strong>從:</strong> {tx.from}
</div>
<div style={{ marginBottom: '5px', fontSize: '13px' }}>
<strong>到:</strong> {tx.to}
</div>
<div style={{ marginBottom: '5px', fontSize: '13px' }}>
<strong>金額:</strong> {tx.value} {tx.tokenSymbol || 'ETH'}
</div>
<div style={{ marginBottom: '5px', fontSize: '13px' }}>
<strong>時間:</strong> {tx.timeStamp}
</div>
<div style={{ fontSize: '13px' }}>
<strong>確認數(shù):</strong> {tx.confirmations}
</div>
</div>
))}
</div>
)}
</Modal>
</Container>
);
};
export default Wallet;
運行入口
import React from 'react';
import Wallet from './Wallet';
function App() {
return (
<div className="App">
<Wallet />
</div>
);
}
export default App;
運行步驟
- 安裝依賴:
npm install ethers crypto-js axios
-
替換代碼中的API密鑰:
- Infura API密鑰:用于連接以太坊網(wǎng)絡(luò)
- Etherscan API密鑰:用于查詢交易歷史
-
啟動應(yīng)用:
npm start
- 在瀏覽器中訪問
http://localhost:3000
5.效果圖

安全特性
- 私鑰全程在客戶端處理,不經(jīng)過網(wǎng)絡(luò)傳輸
- 助記詞強制備份驗證,降低資產(chǎn)丟失風(fēng)險
- 交易前進行余額檢查和地址驗證
- 密碼強度驗證和加密存儲保護私鑰
本文來自博客園,作者:ffffox,轉(zhuǎn)載請注明原文鏈接:http://www.rzrgm.cn/ffffox/p/19020989

浙公網(wǎng)安備 33010602011771號