Ver Fonte

web3.py的客户端添加好了,要进行单元测试。

skyfffire há 5 meses atrás
pai
commit
7ed86a3776
5 ficheiros alterados com 687 adições e 128 exclusões
  1. 2 2
      arbitrage_process.py
  2. 0 0
      ok_chain_client.py
  3. 0 124
      ok_chain_request.js
  4. 2 2
      price_checker_ok.py
  5. 683 0
      web3_py_client.py

+ 2 - 2
arbitrage_system.py → arbitrage_process.py

@@ -4,7 +4,7 @@ import logging
 # 配置日志
 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
-class ArbitrageSystem:
+class ArbitrageProcess:
     def __init__(self, blockchain_client, exchange_client, token_address, quote_token_address, min_arbitrage_amount, slippage_tolerance):
         """
         初始化套利系统
@@ -596,7 +596,7 @@ if __name__ == "__main__":
 
     blockchain_client = MockBlockchainClient()
     exchange_client = MockExchangeClient()
-    arbitrage_system = ArbitrageSystem(
+    arbitrage_system = ArbitrageProcess(
         blockchain_client=blockchain_client,
         exchange_client=exchange_client,
         token_address="0x...TokenA",       # 示例代币地址

+ 0 - 0
ok_chain_request.py → ok_chain_client.py


+ 0 - 124
ok_chain_request.js

@@ -1,124 +0,0 @@
-const https = require('https');
-const crypto = require('crypto');
-const querystring = require('querystring');
-
-// 定义 API 凭证
-const api_config = {
-  "api_key": '4d6b9d92-c43b-4355-94c0-3fc9bb36b0ee',
-  "secret_key": '3C342C0709D461582A140497A5F6E70C',
-  "passphrase": 'Qwe123123.',
-};
-
-function preHash(timestamp, method, request_path, params) {
-  // 根据字符串和参数创建预签名
-  let query_string = '';
-  if (method === 'GET' && params) {
-    query_string = '?' + querystring.stringify(params);
-  }
-  if (method === 'POST' && params) {
-    query_string = JSON.stringify(params);
-  }
-  return timestamp + method + request_path + query_string;
-}
-
-function sign(message, secret_key) {
-  // 使用 HMAC-SHA256 对预签名字符串进行签名
-  const hmac = crypto.createHmac('sha256', secret_key);
-  hmac.update(message);
-  return hmac.digest('base64');
-}
-
-function createSignature(method, request_path, params) {
-  // 获取 ISO 8601 格式时间戳
-  const timestamp = new Date().toISOString().slice(0, -5) + 'Z';
-  // 生成签名
-  const message = preHash(timestamp, method, request_path, params);
-  const signature = sign(message, api_config['secret_key']);
-  return { signature, timestamp };
-}
-
-function sendGetRequest(request_path, params) {
-  // 生成签名
-  const { signature, timestamp } = createSignature("GET", request_path, params);
-
-  // 生成请求头
-  const headers = {
-    'OK-ACCESS-KEY': api_config['api_key'],
-    'OK-ACCESS-SIGN': signature,
-    'OK-ACCESS-TIMESTAMP': timestamp,
-    'OK-ACCESS-PASSPHRASE': api_config['passphrase'],
-  };
-
-  const options = {
-    hostname: 'web3.okx.com',
-    path: request_path + (params ? `?${querystring.stringify(params)}` : ''),
-    method: 'GET',
-    headers: headers
-  };
-
-  const req = https.request(options, (res) => {
-    let data = '';
-    res.on('data', (chunk) => {
-      data += chunk;
-    });
-    res.on('end', () => {
-      console.log(data);
-    });
-  });
-
-  req.end();
-}
-
-function sendPostRequest(request_path, params) {
-  // 生成签名
-  const { signature, timestamp } = createSignature("POST", request_path, params);
-
-  // 生成请求头
-  const headers = {
-    'OK-ACCESS-KEY': api_config['api_key'],
-    'OK-ACCESS-SIGN': signature,
-    'OK-ACCESS-TIMESTAMP': timestamp,
-    'OK-ACCESS-PASSPHRASE': api_config['passphrase'],
-    'Content-Type': 'application/json'
-  };
-
-  const options = {
-    hostname: 'web3.okx.com',
-    path: request_path,
-    method: 'POST',
-    headers: headers
-  };
-
-  const req = https.request(options, (res) => {
-    let data = '';
-    res.on('data', (chunk) => {
-      data += chunk;
-    });
-    res.on('end', () => {
-      console.log(data);
-    });
-  });
-
-  if (params) {
-    req.write(JSON.stringify(params));
-  }
-
-  req.end();
-}
-
-// GET 请求示例
-const getRequestPath = '/api/v5/dex/aggregator/quote';
-const getParams = {
-  'chainId': 42161,
-  'amount': 1000000000000,
-  'toTokenAddress': '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8',
-  'fromTokenAddress': '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'
-};
-console.log(sendGetRequest(getRequestPath, getParams));
-
-// // POST 请求示例
-// const postRequestPath = '/api/v5/mktplace/nft/ordinals/listings';
-// const postParams = {
-//   'slug': 'sats'
-// };
-// sendPostRequest(postRequestPath, postParams);

+ 2 - 2
price_checker_ok.py

@@ -6,7 +6,7 @@ import json
 from flask import Flask, render_template, jsonify
 from collections import deque
 import logging
-import ok_chain_request
+import ok_chain_client
 import pprint
 
 import plotly.graph_objects as go
@@ -32,7 +32,7 @@ def get_chain_price_vs_base_currency(chain_id, in_token_addr, out_token_addr, am
 
     try:
         in_token_amount = amount * (10 ** in_token_decimals)
-        data = ok_chain_request.swap(chain_id, in_token_amount, in_token_addr, out_token_addr, slippage, user_wallet_addr)
+        data = ok_chain_client.swap(chain_id, in_token_amount, in_token_addr, out_token_addr, slippage, user_wallet_addr)
 
         if data.get('code') == '0' and data.get('data'):
             d = data['data'][0];

+ 683 - 0
web3_py_client.py

@@ -0,0 +1,683 @@
+import os
+from web3 import Web3
+from web3.middleware import construct_sign_and_send_tx_middleware
+from web3.exceptions import TransactionNotFound
+from eth_account import Account
+from eth_account.signers.local import LocalAccount
+
+# 定义一个简单的异常类
+class EthTransactionError(Exception):
+    pass
+
+class ERC20Error(Exception):
+    pass
+
+class EthClient:
+    def __init__(self, rpc_url: str):
+        """
+        初始化 EthClient 客户端。
+
+        Args:
+            rpc_url: 以太坊 RPC 节点的 URL (例如: "https://mainnet.infura.io/v3/你的项目ID" 或 "http://localhost:8545")
+        """
+        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
+
+        # 检查 RPC 连接
+        if not self.w3.is_connected():
+            raise ConnectionError(f"无法连接到以太坊 RPC 节点: {rpc_url}")
+        print(f"成功连接到以太坊 RPC 节点: {rpc_url}")
+
+        # 从环境变量加载私钥
+        private_key = os.environ.get("PRIVATE_KEY")
+        if not private_key:
+            raise EnvironmentError("环境变量 'PRIVATE_KEY' 未设置")
+
+        try:
+            self.account: LocalAccount = Account.from_key(private_key)
+            print(f"成功加载私钥,账户地址: {self.account.address}")
+        except Exception as e:
+            raise ValueError(f"加载私钥失败: {e}")
+
+        # 添加签名中间件,使得交易可以被自动签名
+        self.w3.middleware_onion.add(construct_sign_and_send_tx_middleware(self.account))
+
+        # 获取链 ID
+        self.chain_id = self.w3.eth.chain_id
+        print(f"当前连接的链 ID: {self.chain_id}")
+
+    def get_balance(self, address: str) -> int:
+        """
+        获取指定地址的 ETH 余额。
+
+        Args:
+            address: 查询余额的以太坊地址。
+
+        Returns:
+            地址的 ETH 余额 (以最小单位 wei 为单位)。
+        """
+        try:
+            balance = self.w3.eth.get_balance(address)
+            return balance
+        except Exception as e:
+            raise EthTransactionError(f"获取 {address} 的 ETH 余额失败: {e}")
+
+    def get_nonce(self, address: str) -> int:
+        """
+        获取指定地址的下一个可用 nonce。
+
+        Args:
+            address: 查询 nonce 的以太坊地址。
+
+        Returns:
+            地址的下一个可用 nonce。
+        """
+        # 为了提高并发安全性,如果需要频繁发送交易,建议在类的实例中管理 nonce
+        # 或在发送交易前实时获取并处理潜在的 nonce 冲突
+        try:
+            nonce = self.w3.eth.get_transaction_count(address)
+            return nonce
+        except Exception as e:
+            raise EthTransactionError(f"获取 {address} 的 nonce 失败: {e}")
+
+    def estimate_gas(self, transaction: dict) -> int:
+        """
+        估算交易所需的 Gas。
+
+        Args:
+            transaction: 未签名的交易字典。
+
+        Returns:
+            估算的 Gas 量。
+        """
+        # 移除 from 字段,因为估算 Gas 時不需要签名者的 from
+        tx_copy = transaction.copy()
+        tx_copy.pop('from', None)
+        try:
+            gas_estimate = self.w3.eth.estimate_gas(tx_copy)
+            return gas_estimate
+        except Exception as e:
+            # 尝试解析错误信息,提供更具体的提示
+            error_message = str(e)
+            if "execution reverted" in error_message or "revert" in error_message.lower():
+                 raise EthTransactionError(f"估算 Gas 失败,交易可能会 Revert: {error_message}")
+            raise EthTransactionError(f"估算 Gas 失败: {e}")
+
+    def build_eth_transaction(self, to_address: str, amount_ether: float, gas_limit: int = 21000, max_priority_fee_gwei: int = 2, max_fee_per_gas_gwei: int = None) -> dict:
+        # ... (与之前相同,省略代码以保持简洁)
+        """
+        构建一个发送 ETH 的交易字典。
+
+        Args:
+            to_address: 接收 ETH 的地址。
+            amount_ether: 发送的 ETH 数量 (以 Ether 为单位)。
+            gas_limit: 交易的 Gas Limit (发送 ETH 通常为 21000)。
+            max_priority_fee_gwei: EIP-1559 的 Max Priority Fee (以 Gwei 为单位)。
+            max_fee_per_gas_gwei: EIP-1559 的 Max Fee Per Gas (以 Gwei 为单位)。如果为 None,则由节点自动估算。
+
+        Returns:
+            未签名的交易字典。
+        """
+        value_wei = self.w3.to_wei(amount_ether, 'ether')
+        base_fee = self.w3.eth.get_block('latest').baseFeePerGas
+
+        # EIP-1559 Gas Fee Calculation
+        if max_fee_per_gas_gwei is None:
+            max_fee_per_gas_wei = base_fee * 2 + self.w3.to_wei(max_priority_fee_gwei, 'gwei')
+        else:
+            max_fee_per_gas_wei = self.w3.to_wei(max_fee_per_gas_gwei, 'gwei')
+
+        required_min_max_fee = base_fee + self.w3.to_wei(max_priority_fee_gwei, 'gwei')
+        if max_fee_per_gas_wei < required_min_max_fee:
+             max_fee_per_gas_wei = required_min_max_fee
+
+        transaction = {
+            'to': to_address,
+            'value': value_wei,
+            'gas': gas_limit,
+            'maxPriorityFeePerGas': self.w3.to_wei(max_priority_fee_gwei, 'gwei'),
+            'maxFeePerGas': max_fee_per_gas_wei,
+            'nonce': self.get_nonce(self.account.address),
+            'chainId': self.chain_id,
+            'type': 2
+        }
+
+        try:
+            estimated_gas = self.estimate_gas(transaction)
+            print(f"估算 ETH 交易 Gas 量: {estimated_gas}")
+        except EthTransactionError as e:
+             print(f"警告: ETH 交易 Gas 估算失败,使用默认值 {gas_limit}: {e}")
+
+        return transaction
+
+    def send_transaction(self, transaction: dict, wait_for_confirm: bool = True, timeout: int = 180):
+        # ... (与之前相同,省略代码以保持简洁)
+        """
+        发送交易并等待确认(可选)。
+
+        Args:
+            transaction: 未签名的交易字典。
+            wait_for_confirm: 是否等待交易被打包确认。
+            timeout: 等待交易确认的超时时间(秒)。
+
+        Returns:
+            交易哈希字符串。
+        """
+        try:
+            tx_hash = self.w3.eth.send_transaction(transaction)
+            print(f"交易已发送,哈希: {tx_hash.hex()}")
+
+            if wait_for_confirm:
+                print("等待交易确认...")
+                try:
+                    tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
+                    print(f"交易确认成功,区块号: {tx_receipt.blockNumber}")
+                    if tx_receipt.status == 0:
+                        print("警告: 交易失败 (status=0)")
+                        raise EthTransactionError(f"交易失败 (status=0),哈希: {tx_hash.hex()}")
+                    return tx_hash.hex()
+                except TimeoutError:
+                    raise EthTransactionError(f"等待交易确认超时 ({timeout}秒),哈希: {tx_hash.hex()}")
+                except TransactionNotFound:
+                     raise EthTransactionError(f"发送的交易哈希未找到,可能未广播成功或节点同步问题,哈希: {tx_hash.hex()}")
+                except Exception as e:
+                    # 捕获可能的 ContractLogicError 等
+                    raise EthTransactionError(f"等待或处理交易回执时发生错误: {e},哈希: {tx_hash.hex()}")
+            else:
+                return tx_hash.hex()
+
+        except ValueError as e:
+             raise EthTransactionError(f"发送交易时出现值错误: {e}")
+        except Exception as e:
+            # 捕获可能的 RPC 错误等
+             raise EthTransactionError(f"发送交易时发生未知错误: {e}")
+
+    def send_eth(self, to_address: str, amount_ether: float, gas_limit: int = 21000, max_priority_fee_gwei: int = 2, max_fee_per_gas_gwei: int = None, wait_for_confirm: bool = True, timeout: int = 180):
+        # ... (与之前相同,省略代码以保持简洁)
+       """
+       发送原生 ETH。
+
+       Args:
+           to_address: 接收 ETH 的地址。
+           amount_ether: 发送的 ETH 数量 (以 Ether 为单位)。
+           gas_limit: 交易的 Gas Limit。
+           max_priority_fee_gwei: EIP-1559 的 Max Priority Fee (以 Gwei 为单位)。
+           max_fee_per_gas_gwei: EIP-1559 的 Max Fee Per Gas (以 Gwei 为单位)。
+           wait_for_confirm: 是否等待交易被打包确认。
+           timeout: 等待交易确认的超时时间(秒)。
+
+       Returns:
+           交易哈希字符串。
+       """
+       print(f"准备发送 {amount_ether} ETH 到 {to_address}")
+       transaction = self.build_eth_transaction(to_address, amount_ether, gas_limit, max_priority_fee_gwei, max_fee_per_gas_gwei)
+       return self.send_transaction(transaction, wait_for_confirm=wait_for_confirm, timeout=timeout)
+
+    # --- 扩展的 ERC-20 代币功能和 ABI ---
+
+    # 扩展 ERC-20 标准 ABI
+    ERC20_ABI = [
+        # transfer function
+        {
+            "constant": False,
+            "inputs": [
+                {"name": "_to", "type": "address"},
+                {"name": "_value", "type": "uint256"}
+            ],
+            "name": "transfer",
+            "outputs": [{"name": "", "type": "bool"}],
+            "payable": False,
+            "stateMutability": "nonpayable",
+            "type": "function"
+        },
+        # balanceOf function (read-only)
+        {
+            "constant": True,
+            "inputs": [{"name": "_owner", "type": "address"}],
+            "name": "balanceOf",
+            "outputs": [{"name": "balance", "type": "uint256"}],
+            "payable": False,
+            "stateMutability": "view",
+            "type": "function"
+        },
+        # allowance function (read-only)
+        {
+            "constant": True,
+            "inputs": [
+                {"name": "_owner", "type": "address"},
+                {"name": "_spender", "type": "address"}
+            ],
+            "name": "allowance",
+            "outputs": [{"name": "", "type": "uint256"}],
+            "payable": False,
+            "stateMutability": "view",
+            "type": "function"
+        },
+        # approve function (write)
+        {
+            "constant": False,
+            "inputs": [
+                {"name": "_spender", "type": "address"},
+                {"name": "_value", "type": "uint256"}
+            ],
+            "name": "approve",
+            "outputs": [{"name": "", "type": "bool"}],
+            "payable": False,
+            "stateMutability": "nonpayable",
+            "type": "function"
+        },
+        # decimals function (read-only)
+         {
+            "constant": True,
+            "inputs": [],
+            "name": "decimals",
+            "outputs": [{"name": "", "type": "uint8"}],
+            "payable": False,
+            "stateMutability": "view",
+            "type": "function"
+        },
+        # symbol function (read-only)
+        {
+            "constant": True,
+            "inputs": [],
+            "name": "symbol",
+            "outputs": [{"name": "", "type": "string"}],
+            "payable": False,
+            "stateMutability": "view",
+            "type": "function"
+        },
+        # name function (read-only)
+        {
+            "constant": True,
+            "inputs": [],
+            "name": "name",
+            "outputs": [{"name": "", "type": "string"}],
+            "payable": False,
+            "stateMutability": "view",
+            "type": "function"
+        }
+    ]
+
+    def get_erc20_contract(self, token_contract_address: str):
+         """获取 ERC-20 代币合约实例"""
+         if not self.w3.is_address(token_contract_address):
+              raise ERC20Error(f"无效的代币合约地址: {token_contract_address}")
+         try:
+             return self.w3.eth.contract(address=token_contract_address, abi=self.ERC20_ABI)
+         except Exception as e:
+             raise ERC20Error(f"获取代币合约实例失败: {e}")
+
+    def get_erc20_balance(self, token_contract_address: str, account_address: str) -> int:
+        """
+        获取指定账户在指定 ERC-20 代币上的余额。
+
+        Args:
+            token_contract_address: ERC-20 代币合约地址。
+            account_address: 查询余额的以太坊地址。
+
+        Returns:
+            账户的代币余额 (以代币的最小单位为单位)。
+        """
+        if not self.w3.is_address(account_address):
+            raise ValueError(f"无效的账户地址: {account_address}")
+
+        token_contract = self.get_erc20_contract(token_contract_address)
+        try:
+            balance = token_contract.functions.balanceOf(account_address).call()
+            return balance
+        except Exception as e:
+             # 尝试解析 Revert 错误
+             error_message = str(e)
+             if "execution reverted" in error_message or "revert" in error_message.lower():
+                  raise ERC20Error(f"查询 ERC-20 余额失败,可能是合约地址错误或账户不存在: {error_message}")
+             raise ERC20Error(f"查询 ERC-20 余额失败: {e}")
+
+    def get_erc20_allowance(self, token_contract_address: str, owner_address: str, spender_address: str) -> int:
+        """
+        获取 owner 授权给 spender 的代币花费额度。
+
+        Args:
+            token_contract_address: ERC-20 代币合约地址。
+            owner_address: 授权方的地址。
+            spender_address: 被授权方的地址 (通常是另一个智能合约)。
+
+        Returns:
+            授权的代币数量 (以代币的最小单位为单位)。
+        """
+        if not self.w3.is_address(owner_address):
+            raise ValueError(f"无效的 owner 地址: {owner_address}")
+        if not self.w3.is_address(spender_address):
+            raise ValueError(f"无效的 spender 地址: {spender_address}")
+
+        token_contract = self.get_erc20_contract(token_contract_address)
+        try:
+            allowance = token_contract.functions.allowance(owner_address, spender_address).call()
+            return allowance
+        except Exception as e:
+             error_message = str(e)
+             if "execution reverted" in error_message or "revert" in error_message.lower():
+                  raise ERC20Error(f"查询 ERC-20 allowance 失败,可能是合约地址错误或参数错误: {error_message}")
+             raise ERC20Error(f"查询 ERC-20 allowance 失败: {e}")
+
+    def get_erc20_decimals(self, token_contract_address: str) -> int:
+        """
+        获取 ERC-20 代币的 decimals。
+
+        Args:
+            token_contract_address: ERC-20 代币合约地址。
+
+        Returns:
+            代币的 decimal 数量 (通常 18 或 6)。
+        """
+        token_contract = self.get_erc20_contract(token_contract_address)
+        try:
+            decimals = token_contract.functions.decimals().call()
+            return decimals
+        except Exception as e:
+            # 有些不完全符合标准的 ERC-20 可能没有 decimals() 方法
+            # 对于这种情况,可能需要硬编码或使用其他方法获取
+            error_message = str(e)
+            if "execution reverted" in error_message or "revert" in error_message.lower() or "MethodNotFound" in error_message:
+                 raise ERC20Error(f"获取 ERC-20 decimals 失败,可能是合约未实现该方法或地址错误: {error_message}")
+            raise ERC20Error(f"获取 ERC-20 decimals 失败: {e}")
+
+    def get_erc20_symbol(self, token_contract_address: str) -> str:
+        """
+        获取 ERC-20 代币的 symbol。
+
+        Args:
+            token_contract_address: ERC-20 代币合约地址。
+
+        Returns:
+            代币的 symbol 字符串。
+        """
+        token_contract = self.get_erc20_contract(token_contract_address)
+        try:
+            symbol = token_contract.functions.symbol().call()
+            # Web3.py 返回的 bytes 需要解码
+            if isinstance(symbol, bytes):
+                 symbol = symbol.decode('utf-8').replace('\x00', '').strip() # 移除可能的空字节
+            return symbol
+        except Exception as e:
+             error_message = str(e)
+             if "execution reverted" in error_message or "revert" in error_message.lower() or "MethodNotFound" in error_message:
+                  raise ERC20Error(f"获取 ERC-20 symbol 失败,可能是合约未实现该方法或地址错误: {error_message}")
+             raise ERC20Error(f"获取 ERC-20 symbol 失败: {e}")
+
+    def get_erc20_name(self, token_contract_address: str) -> str:
+        """
+        获取 ERC-20 代币的 name。
+
+        Args:
+            token_contract_address: ERC-20 代币合约地址。
+
+        Returns:
+            代币的 name 字符串。
+        """
+        token_contract = self.get_erc20_contract(token_contract_address)
+        try:
+            name = token_contract.functions.name().call()
+            if isinstance(name, bytes):
+                 name = name.decode('utf-8').replace('\x00', '').strip() # 移除可能的空字节
+            return name
+        except Exception as e:
+             error_message = str(e)
+             if "execution reverted" in error_message or "revert" in error_message.lower() or "MethodNotFound" in error_message:
+                  raise ERC20Error(f"获取 ERC-20 name 失败,可能是合约未实现该方法或地址错误: {error_message}")
+             raise ERC20Error(f"获取 ERC-20 name 失败: {e}")
+
+    def build_erc20_transfer_transaction(self, token_contract_address: str, to_address: str, amount: int, gas_limit: int = None, max_priority_fee_gwei: int = 2, max_fee_per_gas_gwei: int = None) -> dict:
+        # ... (与之前相同,但内部调用 get_erc20_contract 获取合约实例)
+        """
+        构建一个发送 ERC-20 代币的交易字典。
+
+        Args:
+            token_contract_address: ERC-20 代币合约地址。
+            to_address: 接收代币的地址。
+            amount: 发送的代币数量 (以代币的最小单位为单位)。
+                         注意: 这不是 Ether 单位,取决于代币的 decimal。
+                         例如,如果代币 decimal 是 18,发送 1 个代币需要传入 10^18。
+            gas_limit: 交易的 Gas Limit。如果为 None,将尝试估算。
+            max_priority_fee_gwei: EIP-1559 的 Max Priority Fee (以 Gwei 为单位)。
+            max_fee_per_gas_gwei: EIP-1559 的 Max Fee Per Gas (以 Gwei 为单位)。
+
+        Returns:
+            未签名的交易字典。
+        """
+        if not self.w3.is_address(to_address):
+             raise ValueError(f"无效的接收地址: {to_address}")
+
+        token_contract = self.get_erc20_contract(token_contract_address)
+
+        transaction = token_contract.functions.transfer(
+            to_address,
+            amount
+        ).build_transaction({
+            'chainId': self.chain_id,
+            'from': self.account.address,
+            'nonce': self.get_nonce(self.account.address),
+            'type': 2
+        })
+
+        base_fee = self.w3.eth.get_block('latest').baseFeePerGas
+
+        if max_fee_per_gas_gwei is None:
+            max_fee_per_gas_wei = base_fee * 2 + self.w3.to_wei(max_priority_fee_gwei, 'gwei')
+        else:
+            max_fee_per_gas_wei = self.w3.to_wei(max_fee_per_gas_gwei, 'gwei')
+
+        required_min_max_fee = base_fee + self.w3.to_wei(max_priority_fee_gwei, 'gwei')
+        if max_fee_per_gas_wei < required_min_max_fee:
+             max_fee_per_gas_wei = required_min_max_fee
+
+        transaction['maxPriorityFeePerGas'] = self.w3.to_wei(max_priority_fee_gwei, 'gwei')
+        transaction['maxFeePerGas'] = max_fee_per_gas_wei
+
+        if gas_limit is None:
+             try:
+                 tx_to_estimate = transaction.copy()
+                 tx_to_estimate['from'] = self.account.address # 估算需要 from 字段
+                 estimated_gas = self.estimate_gas(tx_to_estimate)
+                 transaction['gas'] = estimated_gas
+                 print(f"估算 ERC-20 transfer 交易 Gas 量: {estimated_gas}")
+             except EthTransactionError as e:
+                 print(f"警告: ERC-20 transfer 交易 Gas 估算失败,请手动指定 gas_limit 或检查参数: {e}")
+        else:
+            transaction['gas'] = gas_limit
+
+        return transaction
+
+    def send_erc20_transfer(self, token_contract_address: str, to_address: str, amount: int, gas_limit: int = None, max_priority_fee_gwei: int = 2, max_fee_per_gas_gwei: int = None, wait_for_confirm: bool = True, timeout: int = 180):
+        # ... (与之前相同)
+        """
+        发送 ERC-20 代币。
+
+        Args:
+            token_contract_address: ERC-20 代币合约地址。
+            to_address: 接收代币的地址。
+            amount: 发送的代币数量 (以代币的最小单位为单位)。
+                         注意: 这不是 Ether 单位,取决于代币的 decimal。
+                         例如,如果代币 decimal 是 18,发送 1 个代币需要传入 10^18。
+            gas_limit: 交易的 Gas Limit。如果为 None,将尝试估算。
+            max_priority_fee_gwei: EIP-1559 的 Max Priority Fee (以 Gwei 为单位)。
+            max_fee_per_gas_gwei: EIP-1559 的 Max Fee Per Gas (以 Gwei 为单位)。
+            wait_for_confirm: 是否等待交易被打包确认。
+            timeout: 等待交易确认的超时时间(秒)。
+
+        Returns:
+            交易哈希字符串。
+        """
+        print(f"准备发送 {amount} (以最小单位计) ERC-20 代币从 {token_contract_address} 到 {to_address}")
+        transaction = self.build_erc20_transfer_transaction(
+            token_contract_address,
+            to_address,
+            amount,
+            gas_limit,
+            max_priority_fee_gwei,
+            max_fee_per_gas_gwei
+        )
+        return self.send_transaction(transaction, wait_for_confirm=wait_for_confirm, timeout=timeout)
+
+    def build_erc20_approve_transaction(self, token_contract_address: str, spender_address: str, amount: int, gas_limit: int = None, max_priority_fee_gwei: int = 2, max_fee_per_gas_gwei: int = None) -> dict:
+        """
+        构建一个授权 spender 花费自己代币的交易字典。
+
+        Args:
+            token_contract_address: ERC-20 代币合约地址。
+            spender_address: 被授权 spender 的地址 (通常是另一个智能合约,例如去中心化交易所)。
+            amount: 授权 spender 可以花费的代币数量 (以代币的最小单位为单位)。
+            gas_limit: 交易的 Gas Limit。如果为 None,将尝试估算。
+            max_priority_fee_gwei: EIP-1559 的 Max Priority Fee (以 Gwei 为单位)。
+            max_fee_per_gas_gwei: EIP-1559 的 Max Fee Per Gas (以 Gwei 为单位)。
+
+        Returns:
+            未签名的交易字典。
+        """
+        if not self.w3.is_address(spender_address):
+             raise ValueError(f"无效的 spender 地址: {spender_address}")
+
+        token_contract = self.get_erc20_contract(token_contract_address)
+
+        transaction = token_contract.functions.approve(
+            spender_address,
+            amount
+        ).build_transaction({
+            'chainId': self.chain_id,
+            'from': self.account.address,
+            'nonce': self.get_nonce(self.account.address),
+            'type': 2
+        })
+
+        base_fee = self.w3.eth.get_block('latest').baseFeePerGas
+
+        if max_fee_per_gas_gwei is None:
+            max_fee_per_gas_wei = base_fee * 2 + self.w3.to_wei(max_priority_fee_gwei, 'gwei')
+        else:
+            max_fee_per_gas_wei = self.w3.to_wei(max_fee_per_gas_gwei, 'gwei')
+
+        required_min_max_fee = base_fee + self.w3.to_wei(max_priority_fee_gwei, 'gwei')
+        if max_fee_per_gas_wei < required_min_max_fee:
+             max_fee_per_gas_wei = required_min_max_fee
+
+        transaction['maxPriorityFeePerGas'] = self.w3.to_wei(max_priority_fee_gwei, 'gwei')
+        transaction['maxFeePerGas'] = max_fee_per_gas_wei
+
+        if gas_limit is None:
+             try:
+                 tx_to_estimate = transaction.copy()
+                 tx_to_estimate['from'] = self.account.address # 估算需要 from 字段
+                 estimated_gas = self.estimate_gas(tx_to_estimate)
+                 transaction['gas'] = estimated_gas
+                 print(f"估算 ERC-20 approve 交易 Gas 量: {estimated_gas}")
+             except EthTransactionError as e:
+                 print(f"警告: ERC-20 approve 交易 Gas 估算失败,请手动指定 gas_limit 或检查参数: {e}")
+        else:
+            transaction['gas'] = gas_limit
+
+        return transaction
+
+    def send_erc20_approve(self, token_contract_address: str, spender_address: str, amount: int, gas_limit: int = None, max_priority_fee_gwei: int = 2, max_fee_per_gas_gwei: int = None, wait_for_confirm: bool = True, timeout: int = 180):
+        """
+        授权 spender 花费自己代币。
+
+        Args:
+            token_contract_address: ERC-20 代币合约地址。
+            spender_address: 被授权 spender 的地址 (通常是另一个智能合约,例如去中心化交易所)。
+            amount: 授权 spender 可以花费的代币数量 (以代币的最小单位为单位)。
+            gas_limit: 交易的 Gas Limit。如果为 None,将尝试估算。
+            max_priority_fee_gwei: EIP-1559 的 Max Priority Fee (以 Gwei 为单位)。
+            max_fee_per_gas_gwei: EIP-1559 的 Max Fee Per Gas (以 Gwei 为单位)。
+            wait_for_confirm: 是否等待交易被打包确认。
+            timeout: 等待交易确认的超时时间(秒)。
+
+        Returns:
+            交易哈希字符串。
+        """
+        print(f"准备授权 {spender_address} 花费 {amount} (以最小单位计) ERC-20 代币从 {token_contract_address}")
+        transaction = self.build_erc20_approve_transaction(
+            token_contract_address,
+            spender_address,
+            amount,
+            gas_limit,
+            max_priority_fee_gwei,
+            max_fee_per_gas_gwei
+        )
+        return self.send_transaction(transaction, wait_for_confirm=wait_for_confirm, timeout=timeout)
+
+# --- 使用示例 ---
+
+if __name__ == "__main__":
+    # 请将 YOUR_RPC_URL_HERE 替换为您的实际 RPC 节点 URL
+    rpc_url = "YOUR_RPC_URL_HERE" # 请替换为你的 RPC 节点 URL
+
+    # Sepolia 测试网上的 WETH (Wrapped Ether) 合约地址作为示例
+    # 在真实环境中,你需要获取你想要交互的代币的合约地址
+    sepolia_weth_address = "0x3e57f1cffDD0c89Aec32056fEf5bB60F7c348345" # 示例 WETH 合约地址在 Sepolia
+
+    try:
+        client = EthClient(rpc_url)
+        my_address = client.account.address
+        print(f"我的账户地址: {my_address}")
+
+        # ---- 示例 1: 获取 ERC-20 代币信息 ----
+        try:
+            token_name = client.get_erc20_name(sepolia_weth_address)
+            token_symbol = client.get_erc20_symbol(sepolia_weth_address)
+            token_decimals = client.get_erc20_decimals(sepolia_weth_address)
+            print(f"\n代币名称: {token_name}")
+            print(f"代币符号: {token_symbol}")
+            print(f"代币 decimals: {token_decimals}")
+        except ERC20Error as e:
+             print(f"获取代币信息失败: {e}")
+
+        # ---- 示例 2: 获取 ERC-20 代币余额 ----
+        # 请替换为你要查询余额的地址
+        # address_to_check = "某个地址" # 例如你的地址或另一个地址
+        # try:
+        #     erc20_balance_wei = client.get_erc20_balance(sepolia_weth_address, address_to_check)
+        #     # 将最小单位转换为用户友好的单位
+        #     # 如果知道 decimals,可以计算
+        #     # amount_in_token_units = erc20_balance_wei / (10**token_decimals)
+        #     print(f"\n地址 {address_to_check} 的 {token_symbol} 余额 (最小单位): {erc20_balance_wei}")
+        #     # print(f"地址 {address_to_check} 的 {token_symbol} 余额 ({token_symbol} 单位): {amount_in_token_units}") # 如果 decimals 已知
+        # except ERC20Error as e:
+        #     print(f"获取 ERC-20 余额失败: {e}")
+
+        # ---- 示例 3: 获取 ERC-20 allowance ----
+        # 请替换为 owner 和 spender 地址
+        # owner_address = my_address # 例如你的地址
+        # spender_address = "某个需要花费你代币的合约地址" # 例如 Uniswap router 地址
+        # try:
+        #     erc20_allowance = client.get_erc20_allowance(sepolia_weth_address, owner_address, spender_address)
+        #     print(f"\n地址 {owner_address} 授权给 {spender_address} 的 {token_symbol} 额度 (最小单位): {erc20_allowance}")
+        # except ERC20Error as e:
+        #      print(f"获取 ERC-20 allowance 失败: {e}")
+
+        # ---- 示例 4: 发送 ERC-20 代币交易 ----
+        # 请替换为你要发送的 ERC-20 代币合约地址、目标地址和数量
+        # target_token_address = "目标以太坊地址" # 接收代币的地址
+        # # 发送 0.01 个 WETH (假设 decimals 是 18)
+        # amount_to_send_token = 0.01 * (10**18)
+
+        # print(f"\n尝试发送 ERC-20 代币...")
+        # try:
+        #     tx_hash_erc20_transfer = client.send_erc20_transfer(sepolia_weth_address, target_token_address, int(amount_to_send_token))
+        #     print(f"ERC-20 transfer 交易成功,哈希: {tx_hash_erc20_transfer}")
+        # except EthTransactionError as e:
+        #      print(f"发送 ERC-20 transfer 交易失败: {e}")
+
+        # ---- 示例 5: 发送 ERC-20 approve 交易 ----
+        # 请替换为你要授权的 spender 地址和数量
+        # spender_contract_address = "某个需要你授权代币的合约地址"
+        # # 授权 spender 可以花费 100 个 WETH (假设 decimals 是 18)
+        # amount_to_approve_token = 100 * (10**18)
+        # amount_to_approve_token_max = 2**256 - 1 # 或者授权一个非常大的数 (无限授权)
+
+        # print(f"\n尝试授权 ERC-20 代币花费权限...")
+        # try:
+        #     tx_hash_erc20_approve = client.send_erc20_approve(sepolia_weth_address, spender_contract_address, int(amount_to_approve_token)) # or amount_to_approve_token_max
+        #     print(f"ERC-20 approve 交易成功,哈希: {tx_hash_erc20_approve}")
+        # except EthTransactionError as e:
+        #     print(f"发送 ERC-20 approve 交易失败: {e}")
+
+    except (ConnectionError, EnvironmentError, ValueError, EthTransactionError, ERC20Error) as e:
+        print(f"客户端初始化或操作失败: {e}")