跳到主要内容
基于Python Flask的Web3应用开发 | 极客日志
Python 大前端
基于Python Flask的Web3应用开发 综述由AI生成 使用 Python Flask 和 web3.py 构建 Web3 应用的完整流程。涵盖 Web3 核心概念、环境搭建(Ganache/Hardhat)、项目结构设计、后端 API 实现(余额查询、地址验证)、前端页面交互以及部署注意事项。提供了详细的代码示例,包括 Flask 路由、Web3 连接配置、HTML/CSS/JS 模板及扩展功能(ERC20 代币、交易历史)。适合希望入门区块链后端开发的开发者参考。
栈溢出 发布于 2026/4/5 更新于 2026/6/11 32 浏览基于 Python Flask 的 Web3 应用开发
第一部分:Web3 概述
核心要点总结
1. Web3 的核心概念
数据所有权 :用户真正拥有自己的数据和资产
去中心化 :无需中介即可完成价值转移
可组合性 :应用之间可以无缝交互
2. 技术架构
前端 (HTML/JS) → Flask 后端 → web3.py → 以太坊节点 (Infura)
3. 关键技术栈
Flask : 轻量级 Web 框架
web3.py : Python 的以太坊交互库
Infura/Alchemy : 区块链节点服务
进阶开发建议
1. 安全注意事项
from eth_utils import is_checksum_address
def validate_address (address ):
if not is_checksum_address(address):
pass
PRIVATE_KEY = os.getenv('WALLET_PRIVATE_KEY' )
2. 智能合约交互示例
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({
: ,
: ,
: w3.to_wei( , ),
: nonce,
})
'chainId'
1
'gas'
200000
'gasPrice'
'50'
'gwei'
'nonce'
3. 添加更多功能
@app.route('/get_transactions' )
def get_transactions ():
address = request.args.get('address' )
@app.route('/get_token_balance' )
def get_token_balance ():
@app.route('/send_transaction' , methods=['POST' ] )
def send_transaction ():
4. 项目结构优化 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 # 合约交互
5. 部署注意事项
安全性 :
使用 HTTPS
保护私钥和 API 密钥
实现速率限制
添加输入验证和防注入
@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 )
def get_balance ():
6. 测试建议
from web3 import EthereumTesterProvider
def test_balance_query ():
w3 = Web3(EthereumTesterProvider())
学习路径建议
基础掌握 :
Flask 基础
web3.py 官方文档
以太坊基础概念
进阶学习 :
智能合约开发 (Solidity)
前端集成 (web3.js/ethers.js)
安全最佳实践
项目实践 :
NFT 市场
DeFi 仪表板
DAO 投票系统
多链钱包
这个示例项目虽然简单,但为你展示了 Web3 开发的核心流程。随着 Web3 生态的发展,Python 在区块链数据分析、后端服务和自动化工具方面都有很大应用空间。
第二部分:完整可运行项目设置
1. 环境准备与安装
1.1 系统要求
Python 3.8+
操作系统:Windows/Mac/Linux
至少 2GB 可用内存
1.2 安装 Python 依赖
mkdir flask-web3-demo
cd flask-web3-demo
python -m venv venv
pip install flask web3 python-dotenv requests
1.3 安装以太坊本地测试环境(可选,推荐)
npm install -g ganache
ganache --server.port 8545 --wallet.totalAccounts 10 --wallet.defaultBalance 100
Ganache 会在 http://localhost:8545 启动一个本地以太坊节点,提供 10 个测试账户,每个账户 100ETH。
mkdir hardhat-test
cd hardhat-test
npm init -y
npm install --save-dev hardhat
npx hardhat init
npx hardhat node
2. 项目结构详细说明 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
3. 详细配置文件
3.1 .env.example 文件
INFURA_URL =https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID
ALCHEMY_URL =https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY
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 =0 xYourTestAddressHere
ETHERSCAN_API_KEY =YourEtherscanAPIKey
3.2 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
3.3 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'
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"
CHAIN_ID = 1
TEST_PRIVATE_KEY = os.getenv('TEST_PRIVATE_KEY' )
TEST_ADDRESS = os.getenv('TEST_ADDRESS' )
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()
4. 完整后端代码
4.1 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
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
def init_web3 ():
"""初始化 Web3 连接"""
provider_url = config.get_web3_provider()
print (f"正在连接 Web3 节点:{provider_url} " )
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
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 :
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
balance_wei = w3.eth.get_balance(address)
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:.8 f} ETH"
else :
balance_str = f"{balance_eth:.4 f} 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 :
accounts = w3.eth.accounts
if accounts:
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' : '币安热钱包地址' }
]
if config.is_local_node and w3.is_connected():
try :
accounts = w3.eth.accounts[: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__' :
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 )
5. 前端页面详细实现
5.1 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 >
<link href ="https://cdn.jsdelivr.net/npm/[email protected] /dist/css/bootstrap.min.css" rel ="stylesheet" >
<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 >
<script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/js/bootstrap.bundle.min.js" > </script >
<script src ="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" > </script >
<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 >
5.2 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 %}
6. 运行应用
6.1 启动本地以太坊节点(Ganache)
ganache --server.port 8545 --wallet.totalAccounts 10 --wallet.defaultBalance 100
Ganache CLI v6.12 .2 (ganache-core: 2.13 .2 )
Available Accounts ==================
(0 ) 0 x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 (100 ETH)
(1 ) 0 xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0 (100 ETH)
...
Private Keys ==================
(0 ) 0 x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
...
Listening on 127.0 .0 .1 :8545
6.2 配置环境变量
6.3 启动 Flask 应用 正在连接 Web3 节点:http:
✅ 成功连接到以太坊节点
网络 ID: 1337
最新区块:0
节点:http:
🚀 启动 Flask Web3 应用...
环境:development
调试模式:True
节点:http:
本地节点:True
🌐 请在浏览器中访问:
http:
📊 健康检查:
http:
🔄 网络状态:
http:
* 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:
* Running on http:
6.4 测试应用
打开浏览器访问 http://127.0.0.1:5000
在地址输入框中输入一个以太坊地址,例如:
Ganache 测试地址:0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
以太坊创始人地址:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B
点击"查询余额"按钮
查看查询结果
7. 扩展功能示例
7.1 添加 ERC20 代币余额查询 from web3 import Web3
from eth_abi import decode_abi
import json
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)}
COMMON_TOKENS = {
'USDT' : '0xdAC17F958D2ee523a2206206994597C13D831ec7' ,
'USDC' : '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' ,
'DAI' : '0x6B175474E89094C44Da98b954EedeAC495271d0F' ,
'LINK' : '0x514910771AF9Ca656af840dff83E8264EcF986CA' ,
'UNI' : '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984' ,
'AAVE' : '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9'
}
7.2 添加交易历史查询 @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
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 ]
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
8. 常见问题解决
问题 1:无法连接到节点
检查 Ganache 是否运行:访问 http://localhost:8545
检查端口是否被占用:netstat -ano | findstr :8545
重启 Ganache:ganache --server.port 8545
问题 2:Python 包安装失败
python -m pip install --upgrade pip
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple flask web3
conda install -c conda-forge web3
问题 3:地址格式验证失败
确保地址以 0x 开头
确保地址长度为 42 个字符
使用 Checksum 地址(区分大小写)
问题 4:余额显示为 0
检查是否连接到正确的网络(主网 vs 测试网)
确认地址是否正确
尝试查询示例地址
9. 学习资源
10. 下一步建议
添加更多功能 :
ERC20 代币余额查询
NFT 持有查询
交易发送功能
智能合约交互
改进安全性 :
添加 API 限流
实现 JWT 认证
添加 CORS 配置
使用环境变量管理敏感信息
部署到生产环境 :
使用 Gunicorn 或 uWSGI
配置 Nginx 反向代理
使用 HTTPS
配置数据库存储用户数据
这个完整示例提供了从环境配置到实际运行的所有步骤。你可以基于此项目进一步开发更复杂的 Web3 应用,如 DeFi 仪表板、NFT 市场或 DAO 投票系统。
remove
function
showLoading
containerId
const
document
getElementById
if
style
display
'block'
function
hideLoading
containerId
const
document
getElementById
if
style
display
'none'
function
formatAddress
address
if
length
10
return
return
substring
0
6
'...'
substring
length
4
function
copyToClipboard
text
clipboard
writeText
then
() =>
showMessage
'已复制到剪贴板'
'success'
catch
err =>
console
error
'复制失败:'
showMessage
'复制失败'
'error'
document
addEventListener
'DOMContentLoaded'
function
if
document
getElementById
'toast-container'
const
document
createElement
'div'
id
'toast-container'
className
'toast-container position-fixed top-0 end-0 p-3'
document
body
appendChild
success
const
data
data
const
document
getElementById
'network-status'
className
is_connected
'network-badge network-connected'
'network-badge network-disconnected'
innerHTML
is_connected
'<i class="fas fa-circle"></i>已连接'
'<i class="fas fa-circle"></i>未连接'
if
block_number
document
getElementById
'current-block'
textContent
block_number
if
gas_price_gwei
document
getElementById
'current-gas'
textContent
gas_price_gwei
toFixed
1
if
is_local
innerHTML
' (本地)'
catch
console
error
'获取网络状态失败:'
setTimeout
10000
async
function
validateAddressInput
address
const
document
getElementById
'validationResult'
const
document
getElementById
'validationIcon'
const
document
getElementById
'validationMessage'
if
trim
''
style
display
'none'
return
false
try
const
await
post
'/api/validate-address'
address
const
data
style
display
'block'
if
is_valid
className
'alert alert-success'
innerHTML
'<i class="fas fa-check"></i>'
textContent
'✓ 地址格式正确'
return
true
else
className
'alert alert-danger'
innerHTML
'<i class="fas fa-times"></i>'
textContent
'✗ '
message
return
false
catch
console
error
'验证地址失败:'
return
false
async
function
queryBalance
const
document
getElementById
'addressInput'
value
trim
if
showMessage
'请输入以太坊地址'
'warning'
return
const
await
validateAddressInput
if
showMessage
'地址格式不正确,请检查后重试'
'error'
return
showLoading
'loadingSpinner'
document
getElementById
'resultContainer'
style
display
'none'
try
const
await
post
'/api/balance'
address
if
data
success
displayResult
data
data
showMessage
'查询成功'
'success'
else
showMessage
'查询失败:'
data
error
'error'
catch
console
error
'查询错误:'
const
response
data
error
message
showMessage
'查询失败:'
'error'
finally
hideLoading
'loadingSpinner'
function
displayResult
data
const
document
getElementById
'resultContainer'
document
getElementById
'resultAddress'
textContent
address
document
getElementById
'resultBalance'
textContent
balance_display
document
getElementById
'resultWei'
textContent
balance_wei
' Wei'
const
getNetworkName
network
document
getElementById
'resultNetwork'
textContent
document
getElementById
'resultChainId'
textContent
'Chain ID: '
network
document
getElementById
'resultBlock'
textContent
block_number
const
new
Date
document
getElementById
'resultTimestamp'
textContent
toLocaleTimeString
' '
toLocaleDateString
style
display
'block'
scrollIntoView
behavior
'smooth'
block
'nearest'
function
getNetworkName
chainId
const
1
'以太坊主网'
3
'Ropsten 测试网'
4
'Rinkeby 测试网'
5
'Goerli 测试网'
42
'Kovan 测试网'
1337
'Ganache 本地'
5777
'Ganache 本地'
return
`未知网络 (${chainId} )`
async
function
loadSampleAddresses
try
const
await
get
'/api/sample-addresses'
if
data
success
const
document
getElementById
'sampleAddresses'
innerHTML
''
data
data
forEach
item =>
const
document
createElement
'a'
className
'list-group-item list-group-item-action sample-address'
href
'javascript:void(0)'
innerHTML
`
<div>
<div>
<h6>${item.name} </h6>
<small>${formatAddress(item.address)} </small>
</div>
<small><i class="fas fa-link"></i></small>
</div>
<p>${item.description} </p>
`
addEventListener
'click'
function
document
getElementById
'addressInput'
value
address
validateAddressInput
address
appendChild
catch
console
error
'加载示例地址失败:'
function
copyAddress
if
copyToClipboard
else
showMessage
'没有可复制的地址'
'warning'
async
function
getTestAccount
try
const
await
get
'/api/test-account'
if
data
success
const
data
data
const
document
getElementById
'testAccountInfo'
document
getElementById
'testAddress'
textContent
address
document
getElementById
'testPrivateKey'
textContent
private_key
document
getElementById
'testBalance'
textContent
balance
' ETH'
style
display
'block'
document
getElementById
'addressInput'
value
address
validateAddressInput
address
showMessage
'测试账户已获取'
'success'
else
showMessage
data
error
'error'
catch
console
error
'获取测试账户失败:'
showMessage
'获取测试账户失败'
'error'
async
function
testConnection
try
const
await
get
'/api/network-status'
if
data
success
data
data
is_connected
showMessage
'✅ 连接正常!节点运行中'
'success'
else
showMessage
'❌ 连接失败,请检查节点配置'
'error'
catch
showMessage
'❌ 连接测试失败:'
message
'error'
相关免费在线工具 curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online