ok_chain_client.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import requests
  2. import hmac
  3. import hashlib
  4. import base64
  5. import datetime
  6. import urllib.parse
  7. import json
  8. import time # 只是为了在main的例子中演示循环
  9. try:
  10. from .logger_config import get_logger
  11. except Exception as e:
  12. from logger_config import get_logger
  13. logger = get_logger('as')
  14. # 定义 API 凭证
  15. api_config = {
  16. "api_key": 'a05643ab-fb17-402b-94a8-a886bd343301', # 请替换为您的真实 API Key
  17. "secret_key": '9D59B53EB1E60B1B5F290D3698A8C9DA', # 请替换为您的真实 Secret Key
  18. "passphrase": 'Qwe123123.', # 请替换为您的真实 Passphrase
  19. }
  20. BASE_URL = "https://web3.okx.com"
  21. def get_timestamp():
  22. """
  23. 获取 ISO 8601 格式时间戳,精确到秒,以 'Z' 结尾。
  24. 示例: 2025-05-13T03:55:34Z
  25. """
  26. return datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
  27. def pre_hash_string(timestamp, method, request_path, params_dict=None):
  28. """
  29. 根据字符串和参数创建预签名字符串。
  30. """
  31. params_str = ""
  32. method_upper = method.upper()
  33. if method_upper == 'GET' and params_dict:
  34. # 对GET请求的参数进行URL编码
  35. # OKX V5 API文档通常要求参数按字母顺序排序后进行签名,但您的Node.js示例中querystring.stringify默认不排序。
  36. # 如果API要求排序,可以使用:sorted_params = sorted(params_dict.items())
  37. # params_str = '?' + urllib.parse.urlencode(sorted_params)
  38. # 为了与您的Node.js querystring.stringify行为一致(不排序),我们直接编码
  39. params_str = '?' + urllib.parse.urlencode(params_dict)
  40. elif method_upper == 'POST' and params_dict:
  41. # 对POST请求的body进行JSON字符串化
  42. params_str = json.dumps(params_dict)
  43. # 如果是POST请求但没有body (params_dict is None or empty), params_str 保持为 ""
  44. # 这是OKX API文档通常的要求: "requestBody... The Body of the request (options), "" if it is not a POST request or if there is no request body."
  45. return str(timestamp) + method_upper + request_path + params_str
  46. def sign(message, secret_key):
  47. """
  48. 使用 HMAC-SHA256 对预签名字符串进行签名。
  49. """
  50. mac = hmac.new(secret_key.encode('utf-8'), message.encode('utf-8'), hashlib.sha256)
  51. # digest() 返回 bytes, b64encode() 也返回 bytes, 最后 decode() 成 utf-8 字符串
  52. return base64.b64encode(mac.digest()).decode('utf-8')
  53. def create_signature_headers(method, request_path, params_dict=None):
  54. """
  55. 生成签名及请求头。
  56. """
  57. timestamp = get_timestamp()
  58. message_to_sign = pre_hash_string(timestamp, method, request_path, params_dict)
  59. signature = sign(message_to_sign, api_config['secret_key'])
  60. headers = {
  61. 'OK-ACCESS-KEY': api_config['api_key'],
  62. 'OK-ACCESS-SIGN': signature,
  63. 'OK-ACCESS-TIMESTAMP': timestamp,
  64. 'OK-ACCESS-PASSPHRASE': api_config['passphrase'],
  65. 'Content-Type': 'application/json' # 几乎所有OKX V5 API都推荐或要求这个
  66. }
  67. return headers
  68. def send_get_request(request_path, params_dict=None):
  69. """
  70. 发送GET请求。
  71. """
  72. headers = create_signature_headers("GET", request_path, params_dict)
  73. full_url = BASE_URL + request_path
  74. try:
  75. response = requests.get(full_url, headers=headers, params=params_dict) # requests库会自动处理params的URL编码
  76. response.raise_for_status() # 如果HTTP请求返回了不成功的状态码 (4xx or 5xx),则抛出HTTPError异常
  77. return response.json() # 假设返回的是JSON
  78. except requests.exceptions.HTTPError as http_err:
  79. logger.error(f"HTTP error occurred: {http_err}")
  80. logger.error(f"Response content: {response.content.decode()}")
  81. except requests.exceptions.RequestException as req_err:
  82. logger.error(f"Request exception occurred: {req_err}")
  83. except json.JSONDecodeError:
  84. logger.error(f"Failed to decode JSON. Response content: {response.text}")
  85. return None
  86. def send_post_request(request_path, body_params_dict=None):
  87. """
  88. 发送POST请求。
  89. """
  90. # 对于POST请求,params_dict 用于签名,并且也作为请求体发送
  91. headers = create_signature_headers("POST", request_path, body_params_dict)
  92. full_url = BASE_URL + request_path
  93. try:
  94. # 如果 body_params_dict 为 None 或空字典,requests.post 的 json 参数会发送 "{}" 或不发送body
  95. # 这取决于API如何处理空POST体。通常如果签名时 body为空字符串,post的data也应为空。
  96. # 如果 body_params_dict 为 None, 且签名时也用空字符串表示 body,那么发送 data=""
  97. if body_params_dict:
  98. response = requests.post(full_url, headers=headers, json=body_params_dict)
  99. else:
  100. response = requests.post(full_url, headers=headers, data="") # 如果签名用了空body,这里也用空data
  101. response.raise_for_status()
  102. return response.json()
  103. except requests.exceptions.HTTPError as http_err:
  104. # 首先把這裏的報錯輸出到文件,params也要
  105. logger.error(f"HTTP error occurred: {http_err}")
  106. logger.error(f"Response content: {response.content.decode()}")
  107. except requests.exceptions.RequestException as req_err:
  108. logger.error(f"Request exception occurred: {req_err}")
  109. except json.JSONDecodeError:
  110. logger.error(f"Failed to decode JSON. Response content: {response.text}")
  111. return None
  112. def swap(chain_id, amount, from_token_address, to_token_address, slippage, user_wallet_address, receiver_address=None, gas_level='average'):
  113. get_request_path = '/api/v5/dex/aggregator/swap'
  114. get_params = {
  115. 'chainIndex': chain_id,
  116. 'amount': amount,
  117. 'fromTokenAddress': from_token_address,
  118. 'toTokenAddress': to_token_address,
  119. 'slippage': slippage,
  120. 'userWalletAddress': user_wallet_address,
  121. 'gasLevel': gas_level
  122. }
  123. if receiver_address is not None:
  124. get_params['swapReceiverAddress'] = receiver_address
  125. return send_get_request(get_request_path, get_params)
  126. def approve(chain_id, token_contract_address, approve_amount):
  127. get_request_path = '/api/v5/dex/aggregator/approve-transaction'
  128. get_params = {
  129. 'chainIndex': chain_id,
  130. 'tokenContractAddress': token_contract_address,
  131. 'approveAmount': approve_amount,
  132. }
  133. return send_get_request(get_request_path, get_params)
  134. def history(chain_id, tx_hash):
  135. get_request_path = '/api/v5/dex/aggregator/history'
  136. get_params = {
  137. 'chainIndex': chain_id,
  138. 'txHash': tx_hash
  139. }
  140. return send_get_request(get_request_path, get_params)
  141. def gas_limit(chain_id, from_addr, to_addr, value, call_data=None):
  142. post_request_path = '/api/v5/dex/pre-transaction/gas-limit'
  143. post_params = {
  144. 'chainIndex': chain_id,
  145. 'fromAddress': from_addr,
  146. 'toAddress': to_addr,
  147. 'txAmount': str(value)
  148. }
  149. if call_data is not None:
  150. post_params['extJson'] = call_data
  151. return send_post_request(post_request_path, post_params)
  152. def broadcast(chain_id, address, signed_tx):
  153. post_request_path = '/api/v5/dex/pre-transaction/broadcast-transaction'
  154. post_params = {
  155. 'chainIndex': chain_id,
  156. 'address': address,
  157. 'signedTx': signed_tx
  158. }
  159. return send_post_request(post_request_path, post_params)
  160. def orders(chain_id, address, tx_status=None, order_id=None):
  161. get_request_path = '/api/v5/dex/post-transaction/orders'
  162. get_params = {
  163. 'chainIndex': chain_id,
  164. 'address': address
  165. }
  166. '''
  167. 交易状态:
  168. 1: 排队中
  169. 2: 成功
  170. 3: 失败
  171. '''
  172. if tx_status in [1, 2, 3]:
  173. get_params['txStatus'] = txStatus
  174. if order_id is not None:
  175. get_params['orderId'] = order_id
  176. return send_get_request(get_request_path, get_params)
  177. if __name__ == "__main__":
  178. import decimal
  179. from pprint import pprint
  180. # pprint(swap(1, decimal.Decimal('1000') * (10 ** 6), '0xdAC17F958D2ee523a2206206994597C13D831ec7', '0xf816507E690f5Aa4E29d164885EB5fa7a5627860', 1, '0xf816507E690f5Aa4E29d164885EB5fa7a5627860'))
  181. # # pprint(approve(1, '0xdAC17F958D2ee523a2206206994597C13D831ec7', '1000000'))
  182. # print("测试时间戳格式:", get_timestamp())
  183. # print("-" * 30)
  184. # GET 请求示例 (OKX Web3 API - DEX 聚合器询价)
  185. # 参考文档: https://www.okx.com/web3/build/docs/sdks-and-apis/dex-api/aggregate-and-swap/get-quote
  186. # 注意:这是 Web3 API,其 Base URL 和普通 CEX API 可能不同。
  187. # 确保 BASE_URL 设置为 https://web3.okx.com
  188. # 并且您的 API Key 拥有 Web3 API 的权限。
  189. # 如果是普通 CEX API,BASE_URL 通常是 https://www.okx.com
  190. # logger.info("发送 GET 请求示例:")
  191. # get_request_path = '/api/v5/dex/aggregator/quote'
  192. # get_params = {
  193. # 'chainId': '42161', # Arbitrum One
  194. # 'amount': '1000000000000', # 0.001 WETH (假设WETH是18位小数)
  195. # 'toTokenAddress': '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', # USDC on Arbitrum
  196. # 'fromTokenAddress': '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' # WETH on Arbitrum
  197. # }
  198. # # 确保 API key, secret, passphrase 是针对 web3.okx.com (如果这是你测试的 endpoint)
  199. # # 或者,如果你测试的是 CEX API, 确保 endpoint 和 API key 对应
  200. # logger.info(f"请求路径: {get_request_path}")
  201. # logger.info(f"请求参数: {get_params}")
  202. # response_data_get = send_get_request(get_request_path, get_params)
  203. # if response_data_get:
  204. # logger.info("GET 请求成功,响应:")
  205. # logger.info(json.dumps(response_data_get, indent=2))
  206. # else:
  207. # logger.info("GET 请求失败。")
  208. # logger.info("-" * 30)
  209. # POST 请求示例 (这里用一个CEX API的例子,因为Web3 API 大部分是GET)
  210. # 例如,下单 (Trade API: /api/v5/trade/order)
  211. # BASE_URL 应为 "https://www.okx.com"
  212. # 注意:以下POST示例需要 CEX API 权限,并且请求体结构特定于此接口。仅作演示。
  213. # logger.info("\n发送 POST 请求示例 (CEX Trade API - 下单):")
  214. # BASE_URL_CEX = "https://www.okx.com" # 切换到CEX
  215. # # 重新设置 BASE_URL 以便 send_post_request 使用正确的域
  216. # original_base_url = BASE_URL
  217. # BASE_URL = BASE_URL_CEX
  218. #
  219. # post_request_path_trade = '/api/v5/trade/order'
  220. # post_params_trade = {
  221. # "instId": "BTC-USDT",
  222. # "tdMode": "cash", # "cash" 现货, "cross" 全仓杠杆, "isolated" 逐仓杠杆
  223. # "side": "buy",
  224. # "ordType": "limit",
  225. # "sz": "0.0001", # 数量
  226. # "px": "20000" # 价格
  227. # }
  228. # logger.info(f"请求路径: {post_request_path_trade}")
  229. # logger.info(f"请求体: {post_params_trade}")
  230. # # 确保 API key 对 www.okx.com 有交易权限
  231. # logger.info("--- 警告: 此POST请求会实际下单,请谨慎测试 ---")
  232. # # response_data_post = send_post_request(post_request_path_trade, post_params_trade)
  233. # # if response_data_post:
  234. # # logger.info("POST 请求成功,响应:")
  235. # # logger.info(json.dumps(response_data_post, indent=2))
  236. # # else:
  237. # # logger.info("POST 请求失败。")
  238. #
  239. # BASE_URL = original_base_url # 恢复原始 BASE_URL
  240. # logger.info("-" * 30)
  241. # 如果您想测试 Node.js 代码中注释掉的 POST 示例:
  242. # /api/v5/mktplace/nft/ordinals/listings (这是一个 NFT 市场的 API,也属于 Web3 API)
  243. # BASE_URL 应为 "https://web3.okx.com"
  244. # logger.info("\n发送 POST 请求示例 (Web3 Marketplace Ordinals):")
  245. # post_request_path_ordinals = '/api/v5/mktplace/nft/ordinals/listings'
  246. # post_params_ordinals = {
  247. # 'slug': 'sats' # 注意:此参数可能不完整或不正确,请参照最新API文档
  248. # # 通常获取listings需要更多参数,比如分页,排序等
  249. # }
  250. # logger.info(f"请求路径: {post_request_path_ordinals}")
  251. # logger.info(f"请求体: {post_params_ordinals}")
  252. # response_data_post_ordinals = send_post_request(post_request_path_ordinals, post_params_ordinals)
  253. # if response_data_post_ordinals:
  254. # logger.info("POST 请求成功,响应:")
  255. # logger.info(json.dumps(response_data_post_ordinals, indent=2))
  256. # else:
  257. # logger.info("POST 请求失败。")