ok_chain_client.py 12 KB

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