ok_chain_client.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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. # 定义 API 凭证
  10. api_config = {
  11. "api_key": 'a05643ab-fb17-402b-94a8-a886bd343301', # 请替换为您的真实 API Key
  12. "secret_key": '9D59B53EB1E60B1B5F290D3698A8C9DA', # 请替换为您的真实 Secret Key
  13. "passphrase": 'Qwe123123.', # 请替换为您的真实 Passphrase
  14. }
  15. # api_config = {
  16. # "api_key": '3d34112b-6c78-4a37-8454-096df28bd5d0', # 请替换为您的真实 API Key
  17. # "secret_key": '4E129F37B2836A2C341ECC7C22D1D706', # 请替换为您的真实 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. print(f"HTTP error occurred: {http_err}")
  80. print(f"Response content: {response.content.decode()}")
  81. except requests.exceptions.RequestException as req_err:
  82. print(f"Request exception occurred: {req_err}")
  83. except json.JSONDecodeError:
  84. print(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. print(f"HTTP error occurred: {http_err}")
  105. print(f"Response content: {response.content.decode()}")
  106. except requests.exceptions.RequestException as req_err:
  107. print(f"Request exception occurred: {req_err}")
  108. except json.JSONDecodeError:
  109. print(f"Failed to decode JSON. Response content: {response.text}")
  110. return None
  111. def swap(chain_id, amount, from_token_address, to_token_address, slippage, user_wallet_address, receiver_address=None, gas_level='average'):
  112. get_request_path = '/api/v5/dex/aggregator/swap'
  113. get_params = {
  114. 'chainIndex': chain_id,
  115. 'amount': amount,
  116. 'fromTokenAddress': from_token_address,
  117. 'toTokenAddress': to_token_address,
  118. 'slippage': slippage,
  119. 'userWalletAddress': user_wallet_address,
  120. 'gasLevel': gas_level
  121. }
  122. if receiver_address is not None:
  123. get_params['swapReceiverAddress'] = receiver_address
  124. return send_get_request(get_request_path, get_params)
  125. def approve(chain_id, token_contract_address, approve_amount):
  126. get_request_path = '/api/v5/dex/aggregator/approve-transaction'
  127. get_params = {
  128. 'chainIndex': chain_id,
  129. 'tokenContractAddress': token_contract_address,
  130. 'approveAmount': approve_amount,
  131. }
  132. return send_get_request(get_request_path, get_params)
  133. if __name__ == "__main__":
  134. import decimal
  135. import pprint
  136. pprint.pprint(swap(1, decimal.Decimal('1000') * (10 ** 6), '0xdAC17F958D2ee523a2206206994597C13D831ec7', '0xf816507E690f5Aa4E29d164885EB5fa7a5627860', 1, '0xf816507E690f5Aa4E29d164885EB5fa7a5627860'))
  137. # pprint.pprint(approve(1, '0xdAC17F958D2ee523a2206206994597C13D831ec7', '1000000'))
  138. # print("测试时间戳格式:", get_timestamp())
  139. # print("-" * 30)
  140. # GET 请求示例 (OKX Web3 API - DEX 聚合器询价)
  141. # 参考文档: https://www.okx.com/web3/build/docs/sdks-and-apis/dex-api/aggregate-and-swap/get-quote
  142. # 注意:这是 Web3 API,其 Base URL 和普通 CEX API 可能不同。
  143. # 确保 BASE_URL 设置为 https://web3.okx.com
  144. # 并且您的 API Key 拥有 Web3 API 的权限。
  145. # 如果是普通 CEX API,BASE_URL 通常是 https://www.okx.com
  146. # print("发送 GET 请求示例:")
  147. # get_request_path = '/api/v5/dex/aggregator/quote'
  148. # get_params = {
  149. # 'chainId': '42161', # Arbitrum One
  150. # 'amount': '1000000000000', # 0.001 WETH (假设WETH是18位小数)
  151. # 'toTokenAddress': '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', # USDC on Arbitrum
  152. # 'fromTokenAddress': '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' # WETH on Arbitrum
  153. # }
  154. # # 确保 API key, secret, passphrase 是针对 web3.okx.com (如果这是你测试的 endpoint)
  155. # # 或者,如果你测试的是 CEX API, 确保 endpoint 和 API key 对应
  156. # print(f"请求路径: {get_request_path}")
  157. # print(f"请求参数: {get_params}")
  158. # response_data_get = send_get_request(get_request_path, get_params)
  159. # if response_data_get:
  160. # print("GET 请求成功,响应:")
  161. # print(json.dumps(response_data_get, indent=2))
  162. # else:
  163. # print("GET 请求失败。")
  164. # print("-" * 30)
  165. # POST 请求示例 (这里用一个CEX API的例子,因为Web3 API 大部分是GET)
  166. # 例如,下单 (Trade API: /api/v5/trade/order)
  167. # BASE_URL 应为 "https://www.okx.com"
  168. # 注意:以下POST示例需要 CEX API 权限,并且请求体结构特定于此接口。仅作演示。
  169. # print("\n发送 POST 请求示例 (CEX Trade API - 下单):")
  170. # BASE_URL_CEX = "https://www.okx.com" # 切换到CEX
  171. # # 重新设置 BASE_URL 以便 send_post_request 使用正确的域
  172. # original_base_url = BASE_URL
  173. # BASE_URL = BASE_URL_CEX
  174. #
  175. # post_request_path_trade = '/api/v5/trade/order'
  176. # post_params_trade = {
  177. # "instId": "BTC-USDT",
  178. # "tdMode": "cash", # "cash" 现货, "cross" 全仓杠杆, "isolated" 逐仓杠杆
  179. # "side": "buy",
  180. # "ordType": "limit",
  181. # "sz": "0.0001", # 数量
  182. # "px": "20000" # 价格
  183. # }
  184. # print(f"请求路径: {post_request_path_trade}")
  185. # print(f"请求体: {post_params_trade}")
  186. # # 确保 API key 对 www.okx.com 有交易权限
  187. # print("--- 警告: 此POST请求会实际下单,请谨慎测试 ---")
  188. # # response_data_post = send_post_request(post_request_path_trade, post_params_trade)
  189. # # if response_data_post:
  190. # # print("POST 请求成功,响应:")
  191. # # print(json.dumps(response_data_post, indent=2))
  192. # # else:
  193. # # print("POST 请求失败。")
  194. #
  195. # BASE_URL = original_base_url # 恢复原始 BASE_URL
  196. # print("-" * 30)
  197. # 如果您想测试 Node.js 代码中注释掉的 POST 示例:
  198. # /api/v5/mktplace/nft/ordinals/listings (这是一个 NFT 市场的 API,也属于 Web3 API)
  199. # BASE_URL 应为 "https://web3.okx.com"
  200. # print("\n发送 POST 请求示例 (Web3 Marketplace Ordinals):")
  201. # post_request_path_ordinals = '/api/v5/mktplace/nft/ordinals/listings'
  202. # post_params_ordinals = {
  203. # 'slug': 'sats' # 注意:此参数可能不完整或不正确,请参照最新API文档
  204. # # 通常获取listings需要更多参数,比如分页,排序等
  205. # }
  206. # print(f"请求路径: {post_request_path_ordinals}")
  207. # print(f"请求体: {post_params_ordinals}")
  208. # response_data_post_ordinals = send_post_request(post_request_path_ordinals, post_params_ordinals)
  209. # if response_data_post_ordinals:
  210. # print("POST 请求成功,响应:")
  211. # print(json.dumps(response_data_post_ordinals, indent=2))
  212. # else:
  213. # print("POST 请求失败。")