基于 Python Flask 的 Web3 应用开发
第一部分:Web3 概述
核心要点总结
1. Web3 的核心概念
- 数据所有权:用户真正拥有自己的数据和资产
- 去中心化:无需中介即可完成价值转移
- 可组合性:应用之间可以无缝交互
使用 Python Flask 和 web3.py 构建 Web3 应用的完整流程。涵盖 Web3 核心概念、环境搭建(Ganache/Hardhat)、项目结构设计、后端 API 实现(余额查询、地址验证)、前端页面交互以及部署注意事项。提供了详细的代码示例,包括 Flask 路由、Web3 连接配置、HTML/CSS/JS 模板及扩展功能(ERC20 代币、交易历史)。适合希望入门区块链后端开发的开发者参考。
前端 (HTML/JS) → Flask 后端 → web3.py → 以太坊节点 (Infura)
# 1. 输入验证增强
from eth_utils import is_checksum_address
def validate_address(address):
if not is_checksum_address(address):
# 进一步处理或返回错误
pass
# 2. 环境变量安全
# 不要将私钥硬编码在代码中
PRIVATE_KEY = os.getenv('WALLET_PRIVATE_KEY')
from web3 import Web3
from web3.contract import Contract
# 加载合约
contract_address = "0xContractAddress"
with open('contract_abi.json') as f:
contract_abi = json.load(f)
contract = w3.eth.contract(address=contract_address, abi=contract_abi)
# 读取合约数据
balance = contract.functions.balanceOf(address).call()
# 发送交易(需要私钥签名)
nonce = w3.eth.get_transaction_count(my_address)
transaction = contract.functions.transfer(
to_address,
amount
).build_transaction({
'chainId': 1,
'gas': 200000,
'gasPrice': w3.to_wei('50', 'gwei'),
'nonce': nonce,
})
# 1. 获取交易记录
@app.route('/get_transactions')
def get_transactions():
address = request.args.get('address')
# 使用 Etherscan API 或节点查询
# 2. 获取代币余额
@app.route('/get_token_balance')
def get_token_balance():
# 查询 ERC20 代币余额
# 3. 发送交易
@app.route('/send_transaction', methods=['POST'])
def send_transaction():
# 实现交易发送功能(需安全处理私钥)
flask-web3-demo/
├── app.py # 主应用文件
├── config.py # 配置文件
├── requirements.txt # 依赖包
├── .env # 环境变量
├── static/ # 静态文件
│ ├── css/
│ └── js/
├── templates/ # 模板文件
│ ├── base.html
│ ├── index.html
│ └── dashboard.html
└── blueprints/ # 蓝图模块
├── auth.py # 认证模块
├── wallet.py # 钱包功能
└── contracts.py # 合约交互
错误处理增强:
@app.errorhandler(500)
def internal_error(error):
return jsonify({"error": "Internal server error", "message": "Please try again later"}), 500
性能优化:
# 使用缓存
from flask_caching import Cache
cache = Cache(config={'CACHE_TYPE': 'simple'})
@app.route('/get_balance')
@cache.cached(timeout=30) # 缓存 30 秒
def get_balance():
# 查询余额
# 使用 web3.py 的测试功能
from web3 import EthereumTesterProvider
def test_balance_query():
w3 = Web3(EthereumTesterProvider())
# 测试余额查询逻辑
这个示例项目虽然简单,但为你展示了 Web3 开发的核心流程。随着 Web3 生态的发展,Python 在区块链数据分析、后端服务和自动化工具方面都有很大应用空间。
# 创建项目目录
mkdir flask-web3-demo
cd flask-web3-demo
# 创建虚拟环境(推荐)
python -m venv venv
# 激活虚拟环境
# Windows: venv\Scripts\activate
# Mac/Linux: source venv/bin/activate
# 安装依赖
pip install flask web3 python-dotenv requests
方案 A:使用 Ganache(快速本地测试链)
# 1. 安装 Ganache CLI(命令行版)
npm install -g ganache
# 或安装 Ganache 桌面版:https://trufflesuite.com/ganache/
# 2. 启动 Ganache
ganache --server.port 8545 --wallet.totalAccounts 10 --wallet.defaultBalance 100
Ganache 会在 http://localhost:8545 启动一个本地以太坊节点,提供 10 个测试账户,每个账户 100ETH。
方案 B:使用 Hardhat(更接近主网环境)
# 1. 初始化 Hardhat 项目
mkdir hardhat-test
cd hardhat-test
npm init -y
npm install --save-dev hardhat
# 2. 创建 Hardhat 项目
npx hardhat init
# 选择 "Create a JavaScript project"
# 3. 启动本地节点
npx hardhat node
flask-web3-demo/
├── app.py # 主应用文件
├── config.py # 配置文件
├── requirements.txt # 依赖包列表
├── .env # 环境变量(不要提交到 Git)
├── .env.example # 环境变量示例
├── static/ # 静态文件
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── app.js
├── templates/ # HTML 模板
│ ├── base.html
│ ├── index.html
│ └── error.html
├── contracts/ # 智能合约(可选)
│ └── SimpleToken.sol
├── tests/ # 测试文件
│ └── test_app.py
└── utils/ # 工具函数
└── web3_utils.py
.env.example 文件# 以太坊节点配置
# 使用 Infura(主网)
INFURA_URL=https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID
# 使用 Alchemy(备选)
ALCHEMY_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY
# 本地 Ganache 节点(开发用)
GANACHE_URL=http://localhost:8545
# 应用配置
FLASK_ENV=development
FLASK_DEBUG=true
SECRET_KEY=your-secret-key-here
# 钱包配置(测试用,不要用于主网)
TEST_PRIVATE_KEY=your-test-private-key-here
TEST_ADDRESS=0xYourTestAddressHere
# 可选:Etherscan API(用于查询交易)
ETHERSCAN_API_KEY=YourEtherscanAPIKey
requirements.txt 文件Flask==2.3.3
web3==6.10.0
python-dotenv==1.0.0
requests==2.31.0
eth-account==0.9.0
eth-typing==3.5.0
eth-utils==2.3.0
cryptography==41.0.7
config.py 配置文件import os
from dotenv import load_dotenv
load_dotenv()
class Config:
"""基础配置"""
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
FLASK_ENV = os.getenv('FLASK_ENV', 'development')
DEBUG = os.getenv('FLASK_DEBUG', 'true').lower() == 'true'
# Web3 配置
INFURA_URL = os.getenv('INFURA_URL')
ALCHEMY_URL = os.getenv('ALCHEMY_URL')
GANACHE_URL = os.getenv('GANACHE_URL', 'http://localhost:8545')
# 优先使用本地节点,然后使用免费节点
def get_web3_provider(self):
"""获取 Web3 节点提供者"""
if self.GANACHE_URL:
return self.GANACHE_URL
elif self.ALCHEMY_URL:
return self.ALCHEMY_URL
elif self.INFURA_URL:
return self.INFURA_URL
else:
# 备用公共节点(可能不稳定)
return "https://rpc.ankr.com/eth"
# 网络 ID
CHAIN_ID = 1 # 以太坊主网
# 测试钱包配置
TEST_PRIVATE_KEY = os.getenv('TEST_PRIVATE_KEY')
TEST_ADDRESS = os.getenv('TEST_ADDRESS')
# API Keys
ETHERSCAN_API_KEY = os.getenv('ETHERSCAN_API_KEY')
@property
def is_local_node(self):
"""检查是否使用本地节点"""
return 'localhost' in self.get_web3_provider() or '127.0.0.1' in self.get_web3_provider()
# 创建配置实例
config = Config()
app.py - 主应用文件from flask import Flask, render_template, request, jsonify, session
from web3 import Web3
from web3.exceptions import InvalidAddress
from eth_account import Account
import json
import os
import sys
# 添加项目根目录到 Python 路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from config import config
# 禁用未签名交易的自动签名警告
import warnings
warnings.filterwarnings("ignore")
app = Flask(__name__)
app.config['SECRET_KEY'] = config.SECRET_KEY
# 初始化 Web3 连接
def init_web3():
"""初始化 Web3 连接"""
provider_url = config.get_web3_provider()
print(f"正在连接 Web3 节点:{provider_url}")
# 创建 Web3 实例
w3 = Web3(Web3.HTTPProvider(provider_url))
# 测试连接
try:
if w3.is_connected():
print("✅ 成功连接到以太坊节点")
print(f" 网络 ID: {w3.eth.chain_id}")
print(f" 最新区块:{w3.eth.block_number}")
print(f" 节点:{provider_url}")
else:
print("❌ 连接失败,请检查节点配置")
except Exception as e:
print(f"❌ 连接错误:{e}")
return w3
# 全局 Web3 实例
w3 = init_web3()
# 工具函数
def validate_ethereum_address(address):
"""验证以太坊地址格式"""
if not address:
return False, "地址不能为空"
# 检查长度
if len(address) != 42:
return False, "地址长度应为 42 个字符"
# 检查前缀
if not address.startswith('0x'):
return False, "地址应以 0x 开头"
# 检查十六进制字符
try:
# web3.py 的地址验证
if w3.is_address(address):
return True, "地址格式正确"
else:
return False, "无效的以太坊地址"
except Exception as e:
return False, f"地址验证错误:{str(e)}"
def get_eth_balance(address):
"""获取 ETH 余额"""
try:
# 验证地址
is_valid, message = validate_ethereum_address(address)
if not is_valid:
return None, message
# 获取余额(单位:Wei)
balance_wei = w3.eth.get_balance(address)
# 转换为 ETH
balance_eth = w3.from_wei(balance_wei, 'ether')
# 格式化显示
if balance_eth == 0:
balance_str = "0 ETH"
elif balance_eth < 0.0001:
balance_str = f"{balance_eth:.8f} ETH"
else:
balance_str = f"{balance_eth:.4f} ETH"
return {
'address': address,
'balance_wei': str(balance_wei),
'balance_eth': str(balance_eth),
'balance_display': balance_str,
'network': w3.eth.chain_id,
'block_number': w3.eth.block_number
}, None
except Exception as e:
return None, f"查询余额失败:{str(e)}"
def create_test_account():
"""创建测试账户(仅用于开发)"""
if config.is_local_node:
try:
# 获取 Ganache 的测试账户
accounts = w3.eth.accounts
if accounts:
# 获取第一个账户的私钥(仅 Ganache 可用)
# 注意:实际开发中不要在生产环境使用这种方法
if hasattr(w3.provider, 'ethereum_tester'):
# 如果是测试环境
return {
'address': accounts[0],
'private_key': '测试环境不显示私钥',
'balance': w3.from_wei(w3.eth.get_balance(accounts[0]), 'ether')
}
return {
'address': accounts[0],
'private_key': '请查看 Ganache 界面获取私钥',
'balance': w3.from_wei(w3.eth.get_balance(accounts[0]), 'ether')
}
except:
pass
# 如果没有本地节点,创建一个新账户
account = Account.create()
return {
'address': account.address,
'private_key': account.key.hex(),
'balance': '0'
}
# 路由定义
@app.route('/')
def index():
"""主页"""
# 获取网络状态
try:
network_info = {
'is_connected': w3.is_connected(),
'chain_id': w3.eth.chain_id,
'block_number': w3.eth.block_number,
'gas_price': w3.from_wei(w3.eth.gas_price, 'gwei') if w3.is_connected() else 0,
'is_local': config.is_local_node
}
except:
network_info = {
'is_connected': False,
'chain_id': '未知',
'block_number': 0,
'gas_price': 0,
'is_local': config.is_local_node
}
return render_template('index.html', network=network_info, default_address=config.TEST_ADDRESS)
@app.route('/api/balance', methods=['POST'])
def api_get_balance():
"""API:获取余额"""
data = request.get_json()
if not data or 'address' not in data:
return jsonify({'success': False, 'error': '请提供以太坊地址', 'code': 'MISSING_ADDRESS'}), 400
address = data['address'].strip()
result, error = get_eth_balance(address)
if error:
return jsonify({'success': False, 'error': error, 'code': 'BALANCE_ERROR'}), 400
return jsonify({'success': True, 'data': result})
@app.route('/api/network-status')
def api_network_status():
"""API:获取网络状态"""
try:
return jsonify({
'success': True,
'data': {
'is_connected': w3.is_connected(),
'chain_id': w3.eth.chain_id,
'block_number': w3.eth.block_number,
'gas_price_gwei': float(w3.from_wei(w3.eth.gas_price, 'gwei')),
'node_url': config.get_web3_provider(),
'is_local': config.is_local_node
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/test-account')
def api_test_account():
"""API:获取测试账户(仅开发环境)"""
if not config.is_local_node and config.FLASK_ENV == 'production':
return jsonify({'success': False, 'error': '此功能仅在开发环境可用'}), 403
account = create_test_account()
return jsonify({'success': True, 'data': account})
@app.route('/api/validate-address', methods=['POST'])
def api_validate_address():
"""API:验证地址格式"""
data = request.get_json()
address = data.get('address', '').strip()
is_valid, message = validate_ethereum_address(address)
return jsonify({
'success': is_valid,
'is_valid': is_valid,
'message': message,
'address': address
})
@app.route('/api/sample-addresses')
def api_sample_addresses():
"""API:获取示例地址"""
# 一些知名的以太坊地址(只读,用于测试)
samples = [
{'address': '0x0000000000000000000000000000000000000000', 'name': '零地址', 'description': '以太坊零地址,常用于代币销毁'},
{'address': '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', 'name': 'Vitalik Buterin', 'description': '以太坊创始人之一'},
{'address': '0x28C6c06298d514Db089934071355E5743bf21d60', 'name': 'Binance 14', 'description': '币安热钱包地址'}
]
# 如果是本地环境,添加 Ganache 测试地址
if config.is_local_node and w3.is_connected():
try:
accounts = w3.eth.accounts[:3] # 前 3 个测试账户
for i, addr in enumerate(accounts):
samples.insert(0, {
'address': addr,
'name': f'测试账户 {i+1}',
'description': 'Ganache 本地测试账户'
})
except:
pass
return jsonify({'success': True, 'data': samples})
@app.route('/health')
def health_check():
"""健康检查端点"""
return jsonify({
'status': 'healthy',
'web3_connected': w3.is_connected(),
'flask_env': config.FLASK_ENV,
'version': '1.0.0'
})
# 错误处理
@app.errorhandler(404)
def not_found(error):
return jsonify({'success': False, 'error': '资源未找到'}), 404
@app.errorhandler(500)
def internal_error(error):
return jsonify({'success': False, 'error': '服务器内部错误'}), 500
if __name__ == '__main__':
# 检查 Web3 连接
if not w3.is_connected():
print("⚠️ 警告:Web3 连接失败,部分功能可能不可用")
print(" 请检查:")
print(" 1. Ganache 是否启动(http://localhost:8545)")
print(" 2. 或配置 Infura/Alchemy 节点")
# 运行应用
print(f"\n🚀 启动 Flask Web3 应用...")
print(f" 环境:{config.FLASK_ENV}")
print(f" 调试模式:{config.DEBUG}")
print(f" 节点:{config.get_web3_provider()}")
print(f" 本地节点:{config.is_local_node}")
print(f"\n🌐 请在浏览器中访问:")
print(f" http://127.0.0.1:5000")
print(f"\n📊 健康检查:")
print(f" http://127.0.0.1:5000/health")
print(f"\n🔄 网络状态:")
print(f" http://127.0.0.1:5000/api/network-status")
app.run(host='0.0.0.0', port=5000, debug=config.DEBUG, threaded=True)
templates/base.html - 基础模板<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Flask Web3 应用{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 自定义样式 -->
<style>
:root {
--primary-color: #6366f1;
--success-color: #10b981;
--danger-color: #ef4444;
--warning-color: #f59e0b;
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.card {
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
border: none;
overflow: hidden;
}
.gradient-header {
background: linear-gradient(90deg, var(--primary-color), #8b5cf6);
color: white;
padding: 2rem;
margin-bottom: 2rem;
}
.btn-web3 {
background: linear-gradient(90deg, var(--primary-color), #8b5cf6);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 10px;
font-weight: 600;
transition: all 0.3s;
}
.btn-web3:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(99, 102, 241, 0.4);
}
.eth-balance {
font-size: 2.5rem;
font-weight: bold;
color: var(--success-color);
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.address-box {
background: #f8f9fa;
border-radius: 10px;
padding: 1rem;
font-family: 'Courier New', monospace;
word-break: break-all;
}
.network-badge {
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
font-size: 0.85rem;
}
.network-connected {
background-color: rgba(16, 185, 129, 0.1);
color: var(--success-color);
}
.network-disconnected {
background-color: rgba(239, 68, 68, 0.1);
color: var(--danger-color);
}
.loading-spinner {
display: none;
text-align: center;
padding: 2rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.toast {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
min-width: 250px;
}
.sample-address {
cursor: pointer;
transition: all 0.2s;
}
.sample-address:hover {
background-color: #f0f0f0;
transform: translateX(5px);
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
{% block content %}{% endblock %}
</div>
</div>
</div>
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- 自定义 JavaScript -->
<script>
// 显示消息
function showMessage(message, type = 'info') {
const toastContainer = document.getElementById('toast-container');
if (!toastContainer) return;
const toast = document.createElement('div');
toast.className = `toast align-items-center text-bg-${type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div>
<div>
<i class="me-2">${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}</i> ${message}
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
// 自动移除
toast.addEventListener('hidden.bs.toast', function () {
toast.();
});
}
() {
container = .(containerId);
(container) {
container.. = ;
}
}
() {
container = .(containerId);
(container) {
container.. = ;
}
}
() {
(!address || address. < ) address;
address.(, ) + + address.(address. - );
}
() {
navigator..(text).( {
(, );
}).( {
.(, err);
(, );
});
}
.(, () {
(!.()) {
toastContainer = .();
toastContainer. = ;
toastContainer. = ;
..(toastContainer);
}
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>
templates/index.html - 主页面{% extends "base.html" %}
{% block title %}Web3 以太坊余额查询器{% endblock %}
{% block content %}
<div class="card">
<!-- 头部 -->
<div class="gradient-header text-center">
<h1 class="display-5 fw-bold"><i class="fas fa-coins me-2"></i>Web3 余额查询器</h1>
<p class="lead mb-0">查询任何以太坊地址的 ETH 余额,支持主网和测试网络</p>
<!-- 网络状态 -->
<div class="mt-4">
<span id="network-status" class="network-badge network-disconnected"><i class="fas fa-circle me-1"></i>连接中...</span>
<span id="block-number" class="badge bg-secondary ms-2">区块:<span id="current-block">-</span></span>
<span id="gas-price" class="badge bg-info ms-2">Gas: <span id="current-gas">-</span> Gwei</span>
</div>
</div>
<div class="card-body p-4">
<!-- 查询表单 -->
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="addressInput" class="form-label fw-bold"><i class="fas fa-wallet me-1"></i>以太坊地址</label>
<input type="text" class="form-control form-control-lg" id="addressInput" placeholder="例如:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B" value="{{ default_address or '' }}">
<div class="form-text">请输入完整的以太坊地址(0x 开头,42 个字符)</div>
</div>
</div>
<div class="col-md-4 d-flex align-items-end">
<button onclick="queryBalance()" class="btn btn-web3 w-100 py-3"><i class="fas fa-search me-2"></i>查询余额</button>
</div>
</div>
<!-- 验证状态 -->
<div id="validationResult" class="mb-3" style="display: none;">
<span id="validationIcon" class="me-2"></span><span id="validationMessage"></span>
</div>
<!-- 加载动画 -->
<div id="loadingSpinner" class="loading-spinner">
<div class="spinner"></div>
<p class="mt-3 text-muted">正在查询区块链数据...</p>
</div>
<!-- 结果显示 -->
<div id="resultContainer" class="mt-4" style="display: none;">
<div class="card border-success">
<div class="card-header bg-success text-white"><i class="fas fa-check-circle me-2"></i>查询结果</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-muted">地址</h6>
<div class="address-box mb-3">
<span id="resultAddress"></span>
<button onclick="copyAddress()" class="btn btn-sm btn-outline-secondary float-end"><i class="fas fa-copy"></i></button>
</div>
</div>
<div class="col-md-6">
<h6 class="text-muted">网络</h6>
<div class="mb-3">
<span id="resultNetwork" class="badge bg-primary"></span>
<span id="resultChainId" class="badge bg-secondary ms-1"></span>
</div>
</div>
</div>
<div class="text-center py-4">
<h6 class="text-muted mb-2">ETH 余额</h6>
<div class="eth-balance" id="resultBalance">0 ETH</div>
<small class="text-muted" id="resultWei"></small>
</div>
<div class="row mt-4">
<div class="col-md-6">
<h6 class="text-muted">区块高度</h6>
<p class="h5" id="resultBlock">-</p>
</div>
<div class="col-md-6">
<h6 class="text-muted">更新时间</h6>
<p class="h5" id="resultTimestamp">-</p>
</div>
</div>
</div>
</div>
</div>
<!-- 示例地址 -->
<div class="mt-5">
<h5 class="mb-3"><i class="fas fa-list me-2"></i>示例地址</h5>
<div id="sampleAddresses" class="list-group">
<!-- 动态加载示例地址 -->
</div>
</div>
<!-- 测试账户(仅本地环境) -->
{% if network.is_local %}
<div class="mt-5">
<div class="card border-warning">
<div class="card-header bg-warning"><i class="fas fa-flask me-2"></i>开发环境工具</div>
<div class="card-body">
<p class="card-text">您正在使用本地测试网络(Ganache)。这里有一些测试工具:</p>
<div class="row">
<div class="col-md-6">
<button onclick="getTestAccount()" class="btn btn-outline-warning w-100 mb-2"><i class="fas fa-user-plus me-2"></i>获取测试账户</button>
</div>
<div class="col-md-6">
<button onclick="testConnection()" class="btn btn-outline-info w-100 mb-2"><i class="fas fa-plug me-2"></i>测试连接</button>
</div>
</div>
<div id="testAccountInfo" class="mt-3" style="display: none;">
<div class="alert alert-info">
<h6>测试账户信息</h6>
<p class="mb-1"><strong>地址:</strong><span id="testAddress"></span></p>
<p class="mb-1"><strong>私钥:</strong><span id="testPrivateKey"></span></p>
<p class="mb-0"><strong>余额:</strong><span id="testBalance"></span></p>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- 说明 -->
<div class="mt-5">
<div class="card">
<div class="card-header"><i class="fas fa-info-circle me-2"></i>说明</div>
<div class="card-body">
<ul>
<li>此工具可以查询任何以太坊地址的 ETH 余额</li>
<li>数据直接从区块链节点获取,实时更新</li>
<li>支持以太坊主网、测试网和本地开发网络</li>
<li>不存储任何用户数据,所有查询均为匿名</li>
<li>如需查询 ERC20 代币余额,请使用专业的区块链浏览器</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 全局状态
let currentAddress = '';
// 页面加载完成
document.addEventListener('DOMContentLoaded', function () {
// 初始化网络状态
updateNetworkStatus();
// 加载示例地址
loadSampleAddresses();
// 实时验证地址输入
const addressInput = document.getElementById('addressInput');
addressInput.addEventListener('input', function () {
validateAddressInput(this.value);
});
// 按回车键查询
addressInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
queryBalance();
}
});
// 如果有默认地址,自动验证
if (addressInput.value) {
validateAddressInput(addressInput.value);
}
});
// 更新网络状态
async function updateNetworkStatus() {
try {
const response = await axios.get('/api/network-status');
if (response.data.) {
data = response..;
statusBadge = .();
statusBadge. = data. ? : ;
statusBadge. = data. ? : ;
(data.) {
.(). = data.;
}
(data.) {
.(). = data..();
}
(data.) {
statusBadge. += ;
}
}
} (error) {
.(, error);
}
(updateNetworkStatus, );
}
() {
validationResult = .();
validationIcon = .();
validationMessage = .();
(!address || address.() === ) {
validationResult.. = ;
;
}
{
response = axios.(, { : address });
data = response.;
validationResult.. = ;
(data.) {
validationResult. = ;
validationIcon. = ;
validationMessage. = ;
;
} {
validationResult. = ;
validationIcon. = ;
validationMessage. = + data.;
;
}
} (error) {
.(, error);
;
}
}
() {
address = .()..();
(!address) {
(, );
;
}
isValid = (address);
(!isValid) {
(, );
;
}
currentAddress = address;
();
.().. = ;
{
response = axios.(, { : address });
(response..) {
(response..);
(, );
} {
( + response.., );
}
} (error) {
.(, error);
errorMsg = error.?.?. || error.;
( + errorMsg, );
} {
();
}
}
() {
resultContainer = .();
.(). = data.;
.(). = data.;
.(). = data. + ;
networkName = (data.);
.(). = networkName;
.(). = + data.;
.(). = data.;
now = ();
.(). = now.() + + now.();
resultContainer.. = ;
resultContainer.({ : , : });
}
() {
networks = {
: ,
: ,
: ,
: ,
: ,
: ,
:
};
networks[chainId] || ;
}
() {
{
response = axios.();
(response..) {
container = .();
container. = ;
response...( {
addressItem = .();
addressItem. = ;
addressItem. = ;
addressItem. = ;
addressItem.(, () {
.(). = item.;
(item.);
});
container.(addressItem);
});
}
} (error) {
.(, error);
}
}
() {
(currentAddress) {
(currentAddress);
} {
(, );
}
}
() {
{
response = axios.();
(response..) {
account = response..;
container = .();
.(). = account.;
.(). = account.;
.(). = account. + ;
container.. = ;
.(). = account.;
(account.);
(, );
} {
(response.., );
}
} (error) {
.(, error);
(, );
}
}
() {
{
response = axios.();
(response.. && response...) {
(, );
} {
(, );
}
} (error) {
( + error., );
}
}
</script>
{% endblock %}
# 打开一个新的终端窗口
ganache --server.port 8545 --wallet.totalAccounts 10 --wallet.defaultBalance 100
输出应该类似:
Ganache CLI v6.12.2 (ganache-core: 2.13.2)
Available Accounts ==================
(0) 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 (100 ETH)
(1) 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0 (100 ETH)
...
Private Keys ==================
(0) 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
...
Listening on 127.0.0.1:8545
# 复制环境变量示例文件
cp .env.example .env
# 编辑 .env 文件(可选,如果使用 Ganache 可以不填 INFURA_URL)
# 如果你有 Infura 账号,可以填写 INFURA_URL
# 否则留空,系统会自动使用本地 Ganache 节点
# 在项目根目录运行
python app.py
输出应该类似:
正在连接 Web3 节点:http://localhost:8545
✅ 成功连接到以太坊节点
网络 ID: 1337
最新区块:0
节点:http://localhost:8545
🚀 启动 Flask Web3 应用...
环境:development
调试模式:True
节点:http://localhost:8545
本地节点:True
🌐 请在浏览器中访问:
http://127.0.0.1:5000
📊 健康检查:
http://127.0.0.1:5000/health
🔄 网络状态:
http://127.0.0.1:5000/api/network-status
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.x.x:5000
http://127.0.0.1:50000x90F8bf6A479f320ead074411a4B0e7944Ea8c9C10xAb5801a7D398351b8bE11C439e05C5B3259aeC9B创建 utils/web3_utils.py:
from web3 import Web3
from eth_abi import decode_abi
import json
# ERC20 ABI (简化版)
ERC20_ABI = json.loads('''
[
{
"constant": true,
"inputs": [{"name": "_owner", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "balance", "type": "uint256"}],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{"name": "", "type": "uint8"}],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{"name": "", "type": "string"}],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{"name": "", "type": "string"}],
"type": "function"
}
]
''')
def get_erc20_balance(w3, token_address, wallet_address):
"""获取 ERC20 代币余额"""
try:
# 创建合约实例
contract = w3.eth.contract(
address=Web3.to_checksum_address(token_address),
abi=ERC20_ABI
)
# 获取余额
balance = contract.functions.balanceOf(
Web3.to_checksum_address(wallet_address)
).call()
# 获取代币信息
decimals = contract.functions.decimals().call()
symbol = contract.functions.symbol().call()
name = contract.functions.name().call()
# 计算实际余额
actual_balance = balance / (10 ** decimals)
return {
'success': True,
'balance': str(actual_balance),
'balance_raw': str(balance),
'symbol': symbol,
'name': name,
'decimals': decimals,
'token_address': token_address
}
except Exception as e:
return {'success': False, 'error': str(e)}
# 常用 ERC20 代币地址
COMMON_TOKENS = {
'USDT': '0xdAC17F958D2ee523a2206206994597C13D831ec7',
'USDC': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
'DAI': '0x6B175474E89094C44Da98b954EedeAC495271d0F',
'LINK': '0x514910771AF9Ca656af840dff83E8264EcF986CA',
'UNI': '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984',
'AAVE': '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9'
}
在 app.py 中添加:
@app.route('/api/transactions', methods=['POST'])
def api_get_transactions():
"""获取交易历史(使用 Etherscan API)"""
data = request.get_json()
address = data.get('address', '').strip()
if not address:
return jsonify({'success': False, 'error': '请提供地址'}), 400
# 验证地址
is_valid, message = validate_ethereum_address(address)
if not is_valid:
return jsonify({'success': False, 'error': message}), 400
# 如果有 Etherscan API 密钥,可以调用其 API
if config.ETHERSCAN_API_KEY:
try:
import requests
url = f"https://api.etherscan.io/api"
params = {
'module': 'account',
'action': 'txlist',
'address': address,
'startblock': 0,
'endblock': 99999999,
'sort': 'desc',
'apikey': config.ETHERSCAN_API_KEY
}
response = requests.get(url, params=params)
data = response.json()
if data['status'] == '1':
# 处理交易数据
transactions = data['result'][:10] # 只返回最近 10 笔
return jsonify({
'success': True,
'data': {
'address': address,
'transaction_count': len(transactions),
'transactions': transactions
}
})
else:
return jsonify({'success': False, 'error': data.get('message', '获取交易失败')}), 500
except Exception as e:
return jsonify({'success': False, 'error': f'获取交易历史失败:{str(e)}'}), 500
else:
return jsonify({'success': False, 'error': '未配置 Etherscan API 密钥'}), 501
症状:显示"连接失败"或"查询失败"
解决方案:
http://localhost:8545netstat -ano | findstr :8545ganache --server.port 8545症状:pip install 报错
解决方案:
# 升级 pip
python -m pip install --upgrade pip
# 使用国内镜像源
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple flask web3
# 或使用 conda
conda install -c conda-forge web3
症状:总是提示"无效地址"
解决方案:
0x 开头症状:查询真实地址显示 0 ETH
解决方案:
这个完整示例提供了从环境配置到实际运行的所有步骤。你可以基于此项目进一步开发更复杂的 Web3 应用,如 DeFi 仪表板、NFT 市场或 DAO 投票系统。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online