web3_py_client.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. import os
  2. import json
  3. import logging
  4. import re
  5. from decimal import Decimal, ROUND_DOWN
  6. from web3 import Web3
  7. from web3.middleware import ExtraDataToPOAMiddleware # For PoA networks like Goerli, Sepolia, BSC etc.
  8. from web3.providers import HTTPProvider, WebSocketProvider
  9. from eth_account import Account
  10. from dotenv import load_dotenv
  11. from checker.logger_config import get_logger
  12. from encode_decode import decrypt
  13. from web3_providers import SyncWebSocketProvider
  14. # 配置日志
  15. logger = get_logger('as')
  16. # 加载环境变量
  17. load_dotenv()
  18. # 标准 IERC20 ABI (只包含常用函数)
  19. IERC20_ABI = json.loads('''
  20. [
  21. {
  22. "constant": true,
  23. "inputs": [],
  24. "name": "name",
  25. "outputs": [{"name": "", "type": "string"}],
  26. "payable": false,
  27. "stateMutability": "view",
  28. "type": "function"
  29. },
  30. {
  31. "constant": false,
  32. "inputs": [
  33. {"name": "_spender", "type": "address"},
  34. {"name": "_value", "type": "uint256"}
  35. ],
  36. "name": "approve",
  37. "outputs": [{"name": "", "type": "bool"}],
  38. "payable": false,
  39. "stateMutability": "nonpayable",
  40. "type": "function"
  41. },
  42. {
  43. "constant": true,
  44. "inputs": [],
  45. "name": "totalSupply",
  46. "outputs": [{"name": "", "type": "uint256"}],
  47. "payable": false,
  48. "stateMutability": "view",
  49. "type": "function"
  50. },
  51. {
  52. "constant": false,
  53. "inputs": [
  54. {"name": "_from", "type": "address"},
  55. {"name": "_to", "type": "address"},
  56. {"name": "_value", "type": "uint256"}
  57. ],
  58. "name": "transferFrom",
  59. "outputs": [{"name": "", "type": "bool"}],
  60. "payable": false,
  61. "stateMutability": "nonpayable",
  62. "type": "function"
  63. },
  64. {
  65. "constant": true,
  66. "inputs": [],
  67. "name": "decimals",
  68. "outputs": [{"name": "", "type": "uint8"}],
  69. "payable": false,
  70. "stateMutability": "view",
  71. "type": "function"
  72. },
  73. {
  74. "constant": true,
  75. "inputs": [{"name": "_owner", "type": "address"}],
  76. "name": "balanceOf",
  77. "outputs": [{"name": "balance", "type": "uint256"}],
  78. "payable": false,
  79. "stateMutability": "view",
  80. "type": "function"
  81. },
  82. {
  83. "constant": true,
  84. "inputs": [],
  85. "name": "symbol",
  86. "outputs": [{"name": "", "type": "string"}],
  87. "payable": false,
  88. "stateMutability": "view",
  89. "type": "function"
  90. },
  91. {
  92. "constant": false,
  93. "inputs": [
  94. {"name": "_to", "type": "address"},
  95. {"name": "_value", "type": "uint256"}
  96. ],
  97. "name": "transfer",
  98. "outputs": [{"name": "", "type": "bool"}],
  99. "payable": false,
  100. "stateMutability": "nonpayable",
  101. "type": "function"
  102. },
  103. {
  104. "constant": true,
  105. "inputs": [
  106. {"name": "_owner", "type": "address"},
  107. {"name": "_spender", "type": "address"}
  108. ],
  109. "name": "allowance",
  110. "outputs": [{"name": "", "type": "uint256"}],
  111. "payable": false,
  112. "stateMutability": "view",
  113. "type": "function"
  114. }
  115. ]
  116. ''')
  117. class EthClient:
  118. def __init__(self, rpc_url: str = None, hash: str = None):
  119. self.rpc_url = rpc_url or os.getenv("RPC_URL")
  120. ciphertext = os.getenv("ED_CIPHERTEXT")
  121. initial_vector = os.getenv("ED_INITIAL_VECTOR")
  122. secret_key = os.getenv("ED_SECRET_KEY")
  123. _hash = hash or decrypt(bytes.fromhex(ciphertext), bytes.fromhex(secret_key), bytes.fromhex(initial_vector)).decode('utf-8')
  124. _hash = _hash.replace('0x', '')
  125. _hash = re.sub(r'[^a-fA-F0-9]', '', _hash)
  126. if not self.rpc_url:
  127. raise ValueError("RPC_URL not provided or found in environment variables.")
  128. if not _hash:
  129. raise ValueError("HASH not provided or found in environment variables.")
  130. self.rpc_url = rpc_url or os.getenv("RPC_URL")
  131. if not self.rpc_url:
  132. raise ValueError("RPC_URL must be provided or set in environment variables.")
  133. # --- 主要改动在这里 ---
  134. if self.rpc_url.startswith("ws://") or self.rpc_url.startswith("wss://"):
  135. # 使用我们自定义的同步 WebSocket Provider
  136. provider = SyncWebSocketProvider(self.rpc_url)
  137. logger.info(f"Using custom SyncWebSocketProvider for {self.rpc_url}")
  138. elif self.rpc_url.startswith("http://") or self.rpc_url.startswith("https://"):
  139. provider = HTTPProvider(self.rpc_url)
  140. logger.info(f"Using HTTPProvider for {self.rpc_url}")
  141. else:
  142. raise ValueError(f"Invalid RPC URL scheme: {self.rpc_url}.")
  143. # 使用同步的provider
  144. self.w3 = Web3(provider)
  145. # 检查是否成功连接
  146. if not self.w3.is_connected():
  147. raise ConnectionError(f"Failed to connect to RPC URL: {self.rpc_url}")
  148. # 注入 PoA 中间件的逻辑保持不变
  149. chain_id = self.w3.eth.chain_id
  150. poa_chain_ids = {5, 11155111, 56, 97, 137, 80001, 8453, 42161}
  151. if chain_id in poa_chain_ids:
  152. self.w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
  153. logger.warning(f"Injected ExtraDataToPOAMiddleware for Chain ID: {chain_id}")
  154. self.account = Account.from_key(_hash)
  155. self.address = self.account.address
  156. logger.info(f"EthClient initialized. Address: {self.address}, RPC: {self.rpc_url}, Connected: {self.w3.is_connected()}")
  157. def _get_nonce(self) -> int:
  158. """获取账户的下一个 nonce"""
  159. return self.w3.eth.get_transaction_count(self.address)
  160. def _estimate_gas(self, tx: dict) -> int:
  161. """估算交易的 gas limit"""
  162. return self.w3.eth.estimate_gas(tx)
  163. def _sign(self, tx: dict, gas_limit_multiplier: float = 1.2) -> dict:
  164. """签署并返回簽名結果"""
  165. try:
  166. # 填充 gas 和 nonce (如果未提供)
  167. if 'nonce' not in tx:
  168. logger.info('TODO nonce应该提前管理好')
  169. tx['nonce'] = self._get_nonce()
  170. '''
  171. 在使用 web3.py 手动构建和签名交易时,需要使用支持 EIP-1559 交易类型的签名函数。
  172. 通常,eth_account 库及其 sign_transaction 方法是支持的,只要您提供的交易字典包含了正确的 EIP-1559 字段 (chainId, nonce, to, value, gas, maxFeePerGas, maxPriorityFeePerGas 等)。
  173. 您构建的交易字典可能混合了传统 Gas 字段 (gasPrice) 和 EIP-1559 字段 (maxFeePerGas, maxPriorityFeePerGas)。一个交易只能使用其中一种方式来指定 Gas 费用。
  174. 解决方案: 确保您的交易字典中只有 EIP-1559 相关的 Gas 字段(maxFeePerGas, maxPriorityFeePerGas 和 gas),或者只有传统 Gas 字段(gasPrice 和 gas)。
  175. 不要同时包含 gasPrice 和 maxFeePerGas/maxPriorityFeePerGas。
  176. '''
  177. # # 对于支持 EIP-1559 的网络,应该使用:
  178. # if 'maxPriorityFeePerGas' not in tx and 'maxFeePerGas' not in tx:
  179. # latest_block = self.w3.eth.get_block('latest')
  180. # tx['maxPriorityFeePerGas'] = '1000000000'
  181. # tx['maxFeePerGas'] = int(latest_block['baseFeePerGas']) * 2 + int(tx['maxPriorityFeePerGas'])
  182. if 'chainId' not in tx:
  183. tx['chainId'] = self.w3.eth.chain_id
  184. # 增加成交成功率
  185. tx['gas'] = int(int(tx['gas']) * gas_limit_multiplier)
  186. signed_tx = self.w3.eth.account.sign_transaction(tx, self.account.key)
  187. return signed_tx
  188. except Exception as e:
  189. logger.info(f"Error signing transaction: {e}")
  190. def _sign_and_send_transaction(self, tx: dict, gas_limit_multiplier: float = 1.2) -> str:
  191. """签署并发送交易,返回交易哈希"""
  192. try:
  193. signed_tx = self._sign(tx, gas_limit_multiplier)
  194. tx_hash = self.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
  195. return self.w3.to_hex(tx_hash)
  196. except Exception as e:
  197. logger.info(f"Error signing or sending transaction: {e}")
  198. # 可以进一步处理特定错误,例如 nonce 过低,余额不足等
  199. raise
  200. def wait_for_transaction_receipt(self, tx_hash: str, timeout: int = 120, poll_latency: int = 1):
  201. """等待交易被打包并返回收据"""
  202. try:
  203. receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout, poll_latency=poll_latency)
  204. return receipt
  205. except Exception as e: # Web3.exceptions.TimeExhausted as e
  206. logger.info(f"Transaction {tx_hash} timed out after {timeout} seconds.")
  207. raise
  208. def send_eth(self, to_address: str, amount_ether: float, gas_limit: int = None, gas_price_gwei: float = None) -> str:
  209. """发送 ETH"""
  210. if not self.w3.is_address(to_address):
  211. raise ValueError(f"Invalid recipient address: {to_address}")
  212. value_wei = self.w3.to_wei(amount_ether, 'ether')
  213. tx = {
  214. 'to': self.w3.to_checksum_address(to_address),
  215. 'value': value_wei,
  216. 'from': self.address # web3.py 会自动从签名账户中获取 from,但显式写上更清晰
  217. }
  218. if gas_limit:
  219. tx['gas'] = gas_limit
  220. if gas_price_gwei:
  221. tx['gasPrice'] = self.w3.to_wei(gas_price_gwei, 'gwei')
  222. logger.info(f"Preparing to send {amount_ether} ETH to {to_address}...")
  223. return self._sign_and_send_transaction(tx)
  224. def _get_erc20_contract(self, token_address: str):
  225. """获取 ERC20 合约实例"""
  226. if not self.w3.is_address(token_address):
  227. raise ValueError(f"Invalid token address: {token_address}")
  228. return self.w3.eth.contract(address=self.w3.to_checksum_address(token_address), abi=IERC20_ABI)
  229. def get_erc20_decimals(self, token_address: str) -> int:
  230. """获取 ERC20 代币的精度"""
  231. contract = self._get_erc20_contract(token_address)
  232. return contract.functions.decimals().call()
  233. def _to_token_units(self, token_address: str, amount_readable: float) -> int:
  234. """将可读的代币数量转换为最小单位 (例如 1.0 USDT -> 1000000 if decimals is 6)"""
  235. decimals = self.get_erc20_decimals(token_address)
  236. factor = Decimal(10) ** Decimal(decimals)
  237. return int(Decimal(str(amount_readable)) * factor)
  238. def _from_token_units(self, token_address: str, amount_units: int) -> Decimal:
  239. """将最小单位的代币数量转换为可读数量"""
  240. decimals = self.get_erc20_decimals(token_address)
  241. factor = Decimal(10) ** Decimal(decimals)
  242. return (Decimal(amount_units) / factor).quantize(Decimal('0.1') ** decimals, rounding=ROUND_DOWN)
  243. def transfer_erc20(self, token_address: str, to_address: str, amount_readable: float,
  244. gas_limit: int = None, gas_price: float = None) -> str:
  245. """
  246. 转移 ERC20 代币。
  247. :param token_address: 代币合约地址
  248. :param to_address: 接收者地址
  249. :param amount_readable: 要转移的代币数量 (例如 1.2345)
  250. :param gas_limit: 手动设置 gas limit (可选)
  251. :param gas_price: 手动设置 gas price (可选)
  252. :return: 交易哈希
  253. """
  254. if not self.w3.is_address(to_address):
  255. raise ValueError(f"Invalid recipient address: {to_address}")
  256. contract = self._get_erc20_contract(token_address)
  257. amount_in_smallest_units = self._to_token_units(token_address, amount_readable)
  258. tx_data = contract.functions.transfer(
  259. self.w3.to_checksum_address(to_address),
  260. amount_in_smallest_units
  261. ).build_transaction({
  262. 'from': self.address,
  263. 'nonce': self._get_nonce(),
  264. 'gas': 200000, # 通常 transfer ERC20 消耗 50k-100k gas,可以估算或手动设置
  265. 'gasPrice': self.w3.eth.gas_price # 或手动设置
  266. })
  267. if gas_limit:
  268. tx_data['gas'] = gas_limit
  269. if gas_price:
  270. tx_data['gasPrice'] = gas_price
  271. logger.info(f"Preparing to transfer {amount_readable} of token {token_address} to {to_address}...")
  272. return self._sign_and_send_transaction(tx_data, 1.2, 2)
  273. def approve_erc20(self, token_address: str, spender_address: str, amount_readable: float,
  274. gas_limit: int = None, gas_price_gwei: float = None) -> str:
  275. """
  276. 授权给 spender 地址一定数量的 ERC20 代币。
  277. :param token_address: 代币合约地址
  278. :param spender_address: 被授权者地址
  279. :param amount_readable: 要授权的代币数量 (例如 1.2345)
  280. :param gas_limit: 手动设置 gas limit (可选)
  281. :param gas_price_gwei: 手动设置 gas price Gwei (可选)
  282. :return: 交易哈希
  283. """
  284. if not self.w3.is_address(spender_address):
  285. raise ValueError(f"Invalid spender address: {spender_address}")
  286. contract = self._get_erc20_contract(token_address)
  287. amount_in_smallest_units = self._to_token_units(token_address, amount_readable)
  288. tx_data = contract.functions.approve(
  289. self.w3.to_checksum_address(spender_address),
  290. amount_in_smallest_units
  291. ).build_transaction({
  292. 'from': self.address,
  293. 'nonce': self._get_nonce(),
  294. })
  295. if gas_limit:
  296. tx_data['gas'] = gas_limit
  297. if gas_price_gwei:
  298. tx_data['gasPrice'] = self.w3.to_wei(gas_price_gwei, 'gwei')
  299. logger.info(f"Preparing to approve {amount_readable} of token {token_address} for spender {spender_address}...")
  300. return self._sign_and_send_transaction(tx_data)
  301. def get_erc20_balance(self, token_address: str, owner_address: str = None) -> Decimal:
  302. """获取指定地址的 ERC20 代币余额 (可读数量)"""
  303. target_address = owner_address or self.address
  304. if not self.w3.is_address(target_address):
  305. raise ValueError(f"Invalid owner address: {target_address}")
  306. contract = self._get_erc20_contract(token_address)
  307. balance_units = contract.functions.balanceOf(self.w3.to_checksum_address(target_address)).call()
  308. return self._from_token_units(token_address, balance_units)
  309. def get_erc20_allowance(self, token_address: str, spender_address: str, owner_address: str = None) -> Decimal:
  310. """获取 owner 授权给 spender 的 ERC20 代币数量 (可读数量)"""
  311. target_owner = owner_address or self.address
  312. if not self.w3.is_address(target_owner):
  313. raise ValueError(f"Invalid owner address: {target_owner}")
  314. if not self.w3.is_address(spender_address):
  315. raise ValueError(f"Invalid spender address: {spender_address}")
  316. contract = self._get_erc20_contract(token_address)
  317. allowance_units = contract.functions.allowance(
  318. self.w3.to_checksum_address(target_owner),
  319. self.w3.to_checksum_address(spender_address)
  320. ).call()
  321. return self._from_token_units(token_address, allowance_units)
  322. def get_erc20_total_supply(self, token_address: str) -> Decimal:
  323. """获取 ERC20 代币的总供应量 (可读数量)"""
  324. contract = self._get_erc20_contract(token_address)
  325. total_supply_units = contract.functions.totalSupply().call()
  326. return self._from_token_units(token_address, total_supply_units)
  327. def get_erc20_name(self, token_address: str) -> str:
  328. """获取 ERC20 代币的名称"""
  329. contract = self._get_erc20_contract(token_address)
  330. return contract.functions.name().call()
  331. def get_erc20_symbol(self, token_address: str) -> str:
  332. """获取 ERC20 代币的符号"""
  333. contract = self._get_erc20_contract(token_address)
  334. return contract.functions.symbol().call()
  335. def get_eth_balance(self, address: str = None) -> Decimal:
  336. """获取ETH余额 (单位 Ether)"""
  337. target_address = address or self.address
  338. if not self.w3.is_address(target_address):
  339. raise ValueError(f"Invalid address: {target_address}")
  340. balance_wei = self.w3.eth.get_balance(self.w3.to_checksum_address(target_address))
  341. return self.w3.from_wei(balance_wei, 'ether')
  342. if __name__ == "__main__":
  343. import traceback
  344. import time
  345. from pprint import pprint
  346. from checker import ok_chain_client
  347. # from ok_chain_client import swap, broadcast, orders
  348. from decimal import Decimal
  349. from config import wallet
  350. pprint(ok_chain_client.api_config)
  351. # client = EthClient('wss://ethereum-rpc.publicnode.com')
  352. client = EthClient()
  353. CHAIN_ID = 1
  354. # IN_AMOUNT_TO_QUERY = decimal.Decimal('1')
  355. # IN_TOKEN_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7' # USDT on Ethereum
  356. # IN_TOKEN_DECIMALS = decimal.Decimal(6)
  357. # OUT_TOKEN_ADDRESS = '0xf816507E690f5Aa4E29d164885EB5fa7a5627860' # RATO on Ethereum
  358. USER_WALLET = wallet['user_wallet']
  359. # SLIPPAGE = 1
  360. # USER_EXCHANGE_WALLET = wallet['user_exchange_wallet']
  361. # rst = swap(CHAIN_ID, IN_AMOUNT_TO_QUERY * (10 ** IN_TOKEN_DECIMALS), IN_TOKEN_ADDRESS, OUT_TOKEN_ADDRESS, SLIPPAGE, USER_WALLET, USER_EXCHANGE_WALLET)
  362. # data = rst['data'][0]
  363. # tx = data['tx']
  364. try:
  365. # tx.pop('gasPrice', None)
  366. # tx.pop('value', None)
  367. # tx.pop('minReceiveAmount', None)
  368. # tx.pop('slippage', None)
  369. # tx.pop('maxSpendAmount', None)
  370. # tx.pop('signatureData', None)
  371. tx = {
  372. 'from': USER_WALLET,
  373. 'to': USER_WALLET,
  374. 'gas': '40000',
  375. 'value': 1,
  376. 'maxPriorityFeePerGas': '1800000000'
  377. }
  378. latest_block = client.w3.eth.get_block('latest')
  379. tx['maxPriorityFeePerGas'] = int(tx['maxPriorityFeePerGas'])
  380. tx['maxFeePerGas'] = int(int(latest_block['baseFeePerGas']) * 2 + tx['maxPriorityFeePerGas'])
  381. '''
  382. {'code': '0',
  383. 'data': {'chainId': '1',
  384. 'chainIndex': '1',
  385. 'dexRouter': '0x6088d94c5a40cecd3ae2d4e0710ca687b91c61d0',
  386. 'errorMsg': '',
  387. 'fromAddress': '0xb1f33026db86a86372493a3b124d7123e9045bb4',
  388. 'fromTokenDetails': {'amount': '10000000',
  389. 'symbol': 'USDT',
  390. 'tokenAddress': '0xdac17f958d2ee523a2206206994597c13d831ec7'},
  391. 'gasLimit': '285000',
  392. 'gasPrice': '2526360593',
  393. 'gasUsed': '216783',
  394. 'height': '22586403',
  395. 'referralAmount': '',
  396. 'status': 'success',
  397. 'toAddress': '0xc71835a042f4d870b0f4296cc89caeb921a9f3da',
  398. 'toTokenDetails': {'amount': '655477388084022',
  399. 'symbol': 'RATO',
  400. 'tokenAddress': '0xf816507e690f5aa4e29d164885eb5fa7a5627860'},
  401. 'txFee': '',
  402. 'txHash': '0xe54acd58923685fb132bc0d103b72fa538e59d54dec44b3dc8e0314de7d8b126',
  403. 'txTime': '1748497463',
  404. 'txType': 'swap'},
  405. 'msg': ''}
  406. '''
  407. # pprint(ok_chain_client.history(CHAIN_ID, '0xe54acd58923685fb132bc0d103b72fa538e59d54dec44b3dc8e0314de7d8b126'))
  408. '''
  409. {
  410. 'code': '0',
  411. 'data': {'chainId': '1',
  412. 'chainIndex': '1',
  413. 'errorMsg': 'execution reverted',
  414. 'fromAddress': '0xc36b5466d88d3ebe9e538ed650f61b7f9902e6cc',
  415. 'gasLimit': '285000',
  416. 'gasPrice': '4907400618',
  417. 'gasUsed': '282898',
  418. 'height': '22587649',
  419. 'status': 'fail',
  420. 'txFee': '',
  421. 'txHash': '0x344d2d0a9efdfb46b6130a58a40e117bb3bf6181b03b09b65b4d6cc8256f1e2f',
  422. 'txTime': '1748512535',
  423. 'txType': ''},
  424. 'msg': ''
  425. }
  426. '''
  427. # pprint(ok_chain_client.history(CHAIN_ID, '0x0cf772b631a17e4975e1d722d942057ef6c4c4a3490b3ea3715d637ca0abef66'))
  428. # pprint(ok_chain_client.gas_limit(CHAIN_ID, tx['from'], tx['to'], tx['value']))
  429. pprint(tx)
  430. estimated_gas = client.w3.eth.estimate_gas(tx)
  431. estimated_wei = estimated_gas * (tx['maxPriorityFeePerGas'] + tx['maxFeePerGas'])
  432. estimated_eth = estimated_wei / (10 ** 18)
  433. logger.info(f"估算的燃气量: {estimated_gas}, eth消耗: {estimated_eth}")
  434. logger.info(f"餘額:{client.w3.eth.get_balance(USER_WALLET) / Decimal('1e18')}")
  435. # tx_hash = client._sign_and_send_transaction(tx)
  436. # receipt = client.wait_for_transaction_receipt(tx_hash)
  437. # logger.info(f"{tx_hash} 交易已确认! Status: {'Success' if receipt.status == 1 else 'Failed'}")
  438. # ok api發交易測試
  439. signed_tx = client._sign(tx)
  440. pprint(signed_tx)
  441. tx_hash = client.w3.to_hex(signed_tx.hash)
  442. pprint(tx_hash)
  443. # client.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
  444. # raw_transaction = client.w3.to_hex(signed_tx.raw_transaction)
  445. # broadcast_rst = ok_chain_client.broadcast(CHAIN_ID, USER_WALLET, raw_transaction)
  446. # pprint(broadcast_rst)
  447. # order_id = broadcast_rst['data'][0]['orderId']
  448. # while True:
  449. # wallet_orders = ok_chain_client.orders(CHAIN_ID, USER_WALLET)
  450. # pprint(wallet_orders)
  451. # time.sleep(1)
  452. except Exception as e:
  453. print(f"測試失败: {e}")
  454. traceback.print_exc()
  455. # # --- 使用示例 ---
  456. # # 确保你的 .env 文件配置正确
  457. # # 并且你的账户中有足够的 ETH 来支付 Gas 费
  458. # # 替换为实际的ERC20代币地址和接收者地址 (例如USDT on Sepolia testnet)
  459. # # Sepolia USDT: 0xaA8E23Fb1079EA71e0a56F48S1a3ET28wpD1RLf
  460. # # Sepolia WETH: 0x7b79995e5f793A07Bc00c21412e50Ecn10yt2e_ (错误,应为 0x7b79995e5f793A07Bc00c21412e50Ecn10yt2eH)
  461. # # 更正: Sepolia WETH: 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14 (常用)
  462. # # 有些测试网可能没有标准的USDT,你可以找一个存在的ERC20代币进行测试,或者自己部署一个
  463. # # 为了演示,这里假设使用的是 Sepolia 测试网
  464. # # !!重要!!: 以下地址和代币地址仅为示例, 请替换为您测试网络上的真实地址和代币
  465. # # 如果您在主网操作,请务必小心,并使用小额资金测试。
  466. # TEST_RECIPIENT_ADDRESS = "0xb1f33026db86a86372493a3b124d7123e9045bb4" # 替换为你的测试接收地址
  467. # # Sepolia 上的一个示例 ERC20 token (你可以找一个你有的测试币)
  468. # # 用于测试的代币地址
  469. # TEST_ERC20_TOKEN_ADDRESS_SEPOLIA_LINK = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
  470. # try:
  471. # client = EthClient() # RPC_URL 和 HASH 会从 .env 文件加载
  472. # logger.info(f"\nMy ETH Balance: {client.get_eth_balance()} ETH")
  473. # # 1. 发送 ETH (取消注释以测试, 确保接收地址正确且你有足够ETH)
  474. # # logger.info(f"\nAttempting to send ETH...")
  475. # # eth_tx_hash = client.send_eth(TEST_RECIPIENT_ADDRESS, 0.0001) # 发送 0.0001 ETH
  476. # # logger.info(f"ETH transaction sent! Hash: {eth_tx_hash}")
  477. # # receipt = client.wait_for_transaction_receipt(eth_tx_hash)
  478. # # logger.info(f"ETH transaction confirmed! Status: {'Success' if receipt.status == 1 else 'Failed'}")
  479. # # --- ERC20 操作示例 ---
  480. # # 使用 Sepolia LINK 代币进行演示
  481. # token_address = TEST_ERC20_TOKEN_ADDRESS_SEPOLIA_LINK
  482. # if not client.w3.is_address(token_address): # 简单检查
  483. # logger.info(f"Warning: {token_address} does not look like a valid address. Skipping ERC20 tests.")
  484. # else:
  485. # logger.info(f"\n--- ERC20 Token Operations for: {token_address} ---")
  486. # token_name = client.get_erc20_name(token_address)
  487. # token_symbol = client.get_erc20_symbol(token_address)
  488. # token_decimals = client.get_erc20_decimals(token_address)
  489. # logger.info(f"Token: {token_name} ({token_symbol}), Decimals: {token_decimals}")
  490. # # ERC20余额查询以及基础功能测试(总供应量)
  491. # my_token_balance = client.get_erc20_balance(token_address)
  492. # logger.info(f"My {token_symbol} Balance: {my_token_balance} {token_symbol}")
  493. # total_supply = client.get_erc20_total_supply(token_address)
  494. # logger.info(f"Total Supply of {token_symbol}: {total_supply} {token_symbol}")
  495. # # # 2. ERC20 转账 (取消注释以测试, 确保你有该代币且接收地址正确)
  496. # # amount_to_transfer = 0.01 # 转移 0.01 个代币
  497. # # if my_token_balance >= Decimal(str(amount_to_transfer)):
  498. # # logger.info(f"\nAttempting to transfer {amount_to_transfer} {token_symbol}...")
  499. # # erc20_tx_hash = client.transfer_erc20(token_address, TEST_RECIPIENT_ADDRESS, amount_to_transfer)
  500. # # logger.info(f"{token_symbol} transfer transaction sent! Block: {client.w3.eth.block_number} Hash: {erc20_tx_hash}")
  501. # # receipt = client.wait_for_transaction_receipt(erc20_tx_hash)
  502. # # logger.info(f"{token_symbol} transfer transaction confirmed! Block: {client.w3.eth.block_number} Status: {'Success' if receipt.status == 1 else 'Failed'}")
  503. # # logger.info(f"My new {token_symbol} Balance: {client.get_erc20_balance(token_address)} {token_symbol}")
  504. # # else:
  505. # # logger.info(f"Insufficient {token_symbol} balance to transfer {amount_to_transfer} {token_symbol}.")
  506. # # # 3. ERC20 Approve 和 Allowance (取消注释以测试)
  507. # # spender_for_allowance = '0x156ACd2bc5fC336D59BAAE602a2BD9b5e20D6672' # 可以是任何你想授权的地址
  508. # # amount_to_approve = 74547271788
  509. # # token_address = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
  510. # # # current_allowance = client.get_erc20_allowance(token_address, spender_for_allowance)
  511. # # # logger.info(f"\nCurrent allowance for {spender_for_allowance} to spend my {token_symbol}: {current_allowance} {token_symbol}")
  512. # # # if my_token_balance >= Decimal(str(amount_to_approve)): # 确保有足够的代币去授权(虽然授权本身不消耗代币)
  513. # # logger.info(f"\nAttempting to approve {amount_to_approve} {token_symbol} for spender {spender_for_allowance}...")
  514. # # approve_tx_hash = client.approve_erc20(token_address, spender_for_allowance, amount_to_approve)
  515. # # logger.info(f"{token_symbol} approve transaction sent! Hash: {approve_tx_hash}")
  516. # # receipt = client.wait_for_transaction_receipt(approve_tx_hash)
  517. # # logger.info(f"{token_symbol} approve transaction confirmed! Status: {'Success' if receipt.status == 1 else 'Failed'}")
  518. # # new_allowance = client.get_erc20_allowance(token_address, spender_for_allowance)
  519. # # logger.info(f"New allowance for {spender_for_allowance}: {new_allowance} {token_symbol}")
  520. # # else:
  521. # # logger.info(f"Not enough balance to consider approving {amount_to_approve} {token_symbol} (though approval itself doesn't spend tokens).")
  522. # except ValueError as ve:
  523. # logger.info(f"Configuration Error: {ve}")
  524. # except ConnectionError as ce:
  525. # logger.info(f"Connection Error: {ce}")
  526. # except Exception as e:
  527. # logger.info(f"An unexpected error occurred: {e}")
  528. # import traceback
  529. # traceback.logger.info_exc()