signing.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. import time
  2. from decimal import Decimal
  3. import msgpack
  4. from eth_account import Account
  5. from eth_account.messages import encode_typed_data
  6. from eth_utils import keccak, to_hex
  7. from hyperliquid.utils.types import Cloid, Literal, NotRequired, Optional, TypedDict, Union
  8. Tif = Union[Literal["Alo"], Literal["Ioc"], Literal["Gtc"]]
  9. Tpsl = Union[Literal["tp"], Literal["sl"]]
  10. LimitOrderType = TypedDict("LimitOrderType", {"tif": Tif})
  11. TriggerOrderType = TypedDict("TriggerOrderType", {"triggerPx": float, "isMarket": bool, "tpsl": Tpsl})
  12. TriggerOrderTypeWire = TypedDict("TriggerOrderTypeWire", {"triggerPx": str, "isMarket": bool, "tpsl": Tpsl})
  13. OrderType = TypedDict("OrderType", {"limit": LimitOrderType, "trigger": TriggerOrderType}, total=False)
  14. OrderTypeWire = TypedDict("OrderTypeWire", {"limit": LimitOrderType, "trigger": TriggerOrderTypeWire}, total=False)
  15. OrderRequest = TypedDict(
  16. "OrderRequest",
  17. {
  18. "coin": str,
  19. "is_buy": bool,
  20. "sz": float,
  21. "limit_px": float,
  22. "order_type": OrderType,
  23. "reduce_only": bool,
  24. "cloid": NotRequired[Optional[Cloid]],
  25. },
  26. total=False,
  27. )
  28. OidOrCloid = Union[int, Cloid]
  29. ModifyRequest = TypedDict(
  30. "ModifyRequest",
  31. {
  32. "oid": OidOrCloid,
  33. "order": OrderRequest,
  34. },
  35. total=False,
  36. )
  37. CancelRequest = TypedDict("CancelRequest", {"coin": str, "oid": int})
  38. CancelByCloidRequest = TypedDict("CancelByCloidRequest", {"coin": str, "cloid": Cloid})
  39. Grouping = Union[Literal["na"], Literal["normalTpsl"], Literal["positionTpsl"]]
  40. Order = TypedDict(
  41. "Order", {"asset": int, "isBuy": bool, "limitPx": float, "sz": float, "reduceOnly": bool, "cloid": Optional[Cloid]}
  42. )
  43. OrderWire = TypedDict(
  44. "OrderWire",
  45. {
  46. "a": int,
  47. "b": bool,
  48. "p": str,
  49. "s": str,
  50. "r": bool,
  51. "t": OrderTypeWire,
  52. "c": NotRequired[Optional[str]],
  53. },
  54. )
  55. ModifyWire = TypedDict(
  56. "ModifyWire",
  57. {
  58. "oid": int,
  59. "order": OrderWire,
  60. },
  61. )
  62. ScheduleCancelAction = TypedDict(
  63. "ScheduleCancelAction",
  64. {
  65. "type": Literal["scheduleCancel"],
  66. "time": NotRequired[Optional[int]],
  67. },
  68. )
  69. USD_SEND_SIGN_TYPES = [
  70. {"name": "hyperliquidChain", "type": "string"},
  71. {"name": "destination", "type": "string"},
  72. {"name": "amount", "type": "string"},
  73. {"name": "time", "type": "uint64"},
  74. ]
  75. SPOT_TRANSFER_SIGN_TYPES = [
  76. {"name": "hyperliquidChain", "type": "string"},
  77. {"name": "destination", "type": "string"},
  78. {"name": "token", "type": "string"},
  79. {"name": "amount", "type": "string"},
  80. {"name": "time", "type": "uint64"},
  81. ]
  82. WITHDRAW_SIGN_TYPES = [
  83. {"name": "hyperliquidChain", "type": "string"},
  84. {"name": "destination", "type": "string"},
  85. {"name": "amount", "type": "string"},
  86. {"name": "time", "type": "uint64"},
  87. ]
  88. USD_CLASS_TRANSFER_SIGN_TYPES = [
  89. {"name": "hyperliquidChain", "type": "string"},
  90. {"name": "amount", "type": "string"},
  91. {"name": "toPerp", "type": "bool"},
  92. {"name": "nonce", "type": "uint64"},
  93. ]
  94. SEND_ASSET_SIGN_TYPES = [
  95. {"name": "hyperliquidChain", "type": "string"},
  96. {"name": "destination", "type": "string"},
  97. {"name": "sourceDex", "type": "string"},
  98. {"name": "destinationDex", "type": "string"},
  99. {"name": "token", "type": "string"},
  100. {"name": "amount", "type": "string"},
  101. {"name": "fromSubAccount", "type": "string"},
  102. {"name": "nonce", "type": "uint64"},
  103. ]
  104. TOKEN_DELEGATE_TYPES = [
  105. {"name": "hyperliquidChain", "type": "string"},
  106. {"name": "validator", "type": "address"},
  107. {"name": "wei", "type": "uint64"},
  108. {"name": "isUndelegate", "type": "bool"},
  109. {"name": "nonce", "type": "uint64"},
  110. ]
  111. CONVERT_TO_MULTI_SIG_USER_SIGN_TYPES = [
  112. {"name": "hyperliquidChain", "type": "string"},
  113. {"name": "signers", "type": "string"},
  114. {"name": "nonce", "type": "uint64"},
  115. ]
  116. MULTI_SIG_ENVELOPE_SIGN_TYPES = [
  117. {"name": "hyperliquidChain", "type": "string"},
  118. {"name": "multiSigActionHash", "type": "bytes32"},
  119. {"name": "nonce", "type": "uint64"},
  120. ]
  121. def order_type_to_wire(order_type: OrderType) -> OrderTypeWire:
  122. if "limit" in order_type:
  123. return {"limit": order_type["limit"]}
  124. elif "trigger" in order_type:
  125. return {
  126. "trigger": {
  127. "isMarket": order_type["trigger"]["isMarket"],
  128. "triggerPx": float_to_wire(order_type["trigger"]["triggerPx"]),
  129. "tpsl": order_type["trigger"]["tpsl"],
  130. }
  131. }
  132. raise ValueError("Invalid order type", order_type)
  133. def address_to_bytes(address):
  134. return bytes.fromhex(address[2:] if address.startswith("0x") else address)
  135. def action_hash(action, vault_address, nonce, expires_after):
  136. data = msgpack.packb(action)
  137. data += nonce.to_bytes(8, "big")
  138. if vault_address is None:
  139. data += b"\x00"
  140. else:
  141. data += b"\x01"
  142. data += address_to_bytes(vault_address)
  143. if expires_after is not None:
  144. data += b"\x00"
  145. data += expires_after.to_bytes(8, "big")
  146. return keccak(data)
  147. def construct_phantom_agent(hash, is_mainnet):
  148. return {"source": "a" if is_mainnet else "b", "connectionId": hash}
  149. def l1_payload(phantom_agent):
  150. return {
  151. "domain": {
  152. "chainId": 1337,
  153. "name": "Exchange",
  154. "verifyingContract": "0x0000000000000000000000000000000000000000",
  155. "version": "1",
  156. },
  157. "types": {
  158. "Agent": [
  159. {"name": "source", "type": "string"},
  160. {"name": "connectionId", "type": "bytes32"},
  161. ],
  162. "EIP712Domain": [
  163. {"name": "name", "type": "string"},
  164. {"name": "version", "type": "string"},
  165. {"name": "chainId", "type": "uint256"},
  166. {"name": "verifyingContract", "type": "address"},
  167. ],
  168. },
  169. "primaryType": "Agent",
  170. "message": phantom_agent,
  171. }
  172. def user_signed_payload(primary_type, payload_types, action):
  173. chain_id = int(action["signatureChainId"], 16)
  174. return {
  175. "domain": {
  176. "name": "HyperliquidSignTransaction",
  177. "version": "1",
  178. "chainId": chain_id,
  179. "verifyingContract": "0x0000000000000000000000000000000000000000",
  180. },
  181. "types": {
  182. primary_type: payload_types,
  183. "EIP712Domain": [
  184. {"name": "name", "type": "string"},
  185. {"name": "version", "type": "string"},
  186. {"name": "chainId", "type": "uint256"},
  187. {"name": "verifyingContract", "type": "address"},
  188. ],
  189. },
  190. "primaryType": primary_type,
  191. "message": action,
  192. }
  193. def sign_l1_action(wallet, action, active_pool, nonce, expires_after, is_mainnet):
  194. hash = action_hash(action, active_pool, nonce, expires_after)
  195. phantom_agent = construct_phantom_agent(hash, is_mainnet)
  196. data = l1_payload(phantom_agent)
  197. return sign_inner(wallet, data)
  198. def sign_user_signed_action(wallet, action, payload_types, primary_type, is_mainnet):
  199. # signatureChainId is the chain used by the wallet to sign and can be any chain.
  200. # hyperliquidChain determines the environment and prevents replaying an action on a different chain.
  201. action["signatureChainId"] = "0x66eee"
  202. action["hyperliquidChain"] = "Mainnet" if is_mainnet else "Testnet"
  203. data = user_signed_payload(primary_type, payload_types, action)
  204. return sign_inner(wallet, data)
  205. def add_multi_sig_types(sign_types):
  206. enriched_sign_types = []
  207. enriched = False
  208. for sign_type in sign_types:
  209. enriched_sign_types.append(sign_type)
  210. if sign_type["name"] == "hyperliquidChain":
  211. enriched = True
  212. enriched_sign_types.append(
  213. {
  214. "name": "payloadMultiSigUser",
  215. "type": "address",
  216. }
  217. )
  218. enriched_sign_types.append(
  219. {
  220. "name": "outerSigner",
  221. "type": "address",
  222. }
  223. )
  224. if not enriched:
  225. print('"hyperliquidChain" missing from sign_types. sign_types was not enriched with multi-sig signing types')
  226. return enriched_sign_types
  227. def add_multi_sig_fields(action, payload_multi_sig_user, outer_signer):
  228. action = action.copy()
  229. action["payloadMultiSigUser"] = payload_multi_sig_user.lower()
  230. action["outerSigner"] = outer_signer.lower()
  231. return action
  232. def sign_multi_sig_user_signed_action_payload(
  233. wallet, action, is_mainnet, sign_types, tx_type, payload_multi_sig_user, outer_signer
  234. ):
  235. envelope = add_multi_sig_fields(action, payload_multi_sig_user, outer_signer)
  236. sign_types = add_multi_sig_types(sign_types)
  237. return sign_user_signed_action(
  238. wallet,
  239. envelope,
  240. sign_types,
  241. tx_type,
  242. is_mainnet,
  243. )
  244. def sign_multi_sig_l1_action_payload(
  245. wallet, action, is_mainnet, vault_address, timestamp, expires_after, payload_multi_sig_user, outer_signer
  246. ):
  247. envelope = [payload_multi_sig_user.lower(), outer_signer.lower(), action]
  248. return sign_l1_action(
  249. wallet,
  250. envelope,
  251. vault_address,
  252. timestamp,
  253. expires_after,
  254. is_mainnet,
  255. )
  256. def sign_multi_sig_action(wallet, action, is_mainnet, vault_address, nonce, expires_after):
  257. action_without_tag = action.copy()
  258. del action_without_tag["type"]
  259. multi_sig_action_hash = action_hash(action_without_tag, vault_address, nonce, expires_after)
  260. envelope = {
  261. "multiSigActionHash": multi_sig_action_hash,
  262. "nonce": nonce,
  263. }
  264. return sign_user_signed_action(
  265. wallet,
  266. envelope,
  267. MULTI_SIG_ENVELOPE_SIGN_TYPES,
  268. "HyperliquidTransaction:SendMultiSig",
  269. is_mainnet,
  270. )
  271. def sign_usd_transfer_action(wallet, action, is_mainnet):
  272. return sign_user_signed_action(
  273. wallet,
  274. action,
  275. USD_SEND_SIGN_TYPES,
  276. "HyperliquidTransaction:UsdSend",
  277. is_mainnet,
  278. )
  279. def sign_spot_transfer_action(wallet, action, is_mainnet):
  280. return sign_user_signed_action(
  281. wallet,
  282. action,
  283. SPOT_TRANSFER_SIGN_TYPES,
  284. "HyperliquidTransaction:SpotSend",
  285. is_mainnet,
  286. )
  287. def sign_withdraw_from_bridge_action(wallet, action, is_mainnet):
  288. return sign_user_signed_action(
  289. wallet,
  290. action,
  291. WITHDRAW_SIGN_TYPES,
  292. "HyperliquidTransaction:Withdraw",
  293. is_mainnet,
  294. )
  295. def sign_usd_class_transfer_action(wallet, action, is_mainnet):
  296. return sign_user_signed_action(
  297. wallet,
  298. action,
  299. USD_CLASS_TRANSFER_SIGN_TYPES,
  300. "HyperliquidTransaction:UsdClassTransfer",
  301. is_mainnet,
  302. )
  303. def sign_send_asset_action(wallet, action, is_mainnet):
  304. return sign_user_signed_action(
  305. wallet,
  306. action,
  307. SEND_ASSET_SIGN_TYPES,
  308. "HyperliquidTransaction:SendAsset",
  309. is_mainnet,
  310. )
  311. def sign_convert_to_multi_sig_user_action(wallet, action, is_mainnet):
  312. return sign_user_signed_action(
  313. wallet,
  314. action,
  315. CONVERT_TO_MULTI_SIG_USER_SIGN_TYPES,
  316. "HyperliquidTransaction:ConvertToMultiSigUser",
  317. is_mainnet,
  318. )
  319. def sign_agent(wallet, action, is_mainnet):
  320. return sign_user_signed_action(
  321. wallet,
  322. action,
  323. [
  324. {"name": "hyperliquidChain", "type": "string"},
  325. {"name": "agentAddress", "type": "address"},
  326. {"name": "agentName", "type": "string"},
  327. {"name": "nonce", "type": "uint64"},
  328. ],
  329. "HyperliquidTransaction:ApproveAgent",
  330. is_mainnet,
  331. )
  332. def sign_approve_builder_fee(wallet, action, is_mainnet):
  333. return sign_user_signed_action(
  334. wallet,
  335. action,
  336. [
  337. {"name": "hyperliquidChain", "type": "string"},
  338. {"name": "maxFeeRate", "type": "string"},
  339. {"name": "builder", "type": "address"},
  340. {"name": "nonce", "type": "uint64"},
  341. ],
  342. "HyperliquidTransaction:ApproveBuilderFee",
  343. is_mainnet,
  344. )
  345. def sign_token_delegate_action(wallet, action, is_mainnet):
  346. return sign_user_signed_action(
  347. wallet,
  348. action,
  349. TOKEN_DELEGATE_TYPES,
  350. "HyperliquidTransaction:TokenDelegate",
  351. is_mainnet,
  352. )
  353. def sign_inner(wallet, data):
  354. structured_data = encode_typed_data(full_message=data)
  355. signed = wallet.sign_message(structured_data)
  356. return {"r": to_hex(signed["r"]), "s": to_hex(signed["s"]), "v": signed["v"]}
  357. def recover_agent_or_user_from_l1_action(action, signature, active_pool, nonce, expires_after, is_mainnet):
  358. hash = action_hash(action, active_pool, nonce, expires_after)
  359. phantom_agent = construct_phantom_agent(hash, is_mainnet)
  360. data = l1_payload(phantom_agent)
  361. structured_data = encode_typed_data(full_message=data)
  362. address = Account.recover_message(structured_data, vrs=[signature["v"], signature["r"], signature["s"]])
  363. return address
  364. def recover_user_from_user_signed_action(action, signature, payload_types, primary_type, is_mainnet):
  365. action["hyperliquidChain"] = "Mainnet" if is_mainnet else "Testnet"
  366. data = user_signed_payload(primary_type, payload_types, action)
  367. structured_data = encode_typed_data(full_message=data)
  368. address = Account.recover_message(structured_data, vrs=[signature["v"], signature["r"], signature["s"]])
  369. return address
  370. def float_to_wire(x: float) -> str:
  371. rounded = f"{x:.8f}"
  372. if abs(float(rounded) - x) >= 1e-12:
  373. raise ValueError("float_to_wire causes rounding", x)
  374. if rounded == "-0":
  375. rounded = "0"
  376. normalized = Decimal(rounded).normalize()
  377. return f"{normalized:f}"
  378. def float_to_int_for_hashing(x: float) -> int:
  379. return float_to_int(x, 8)
  380. def float_to_usd_int(x: float) -> int:
  381. return float_to_int(x, 6)
  382. def float_to_int(x: float, power: int) -> int:
  383. with_decimals = x * 10**power
  384. if abs(round(with_decimals) - with_decimals) >= 1e-3:
  385. raise ValueError("float_to_int causes rounding", x)
  386. res: int = round(with_decimals)
  387. return res
  388. def get_timestamp_ms() -> int:
  389. return int(time.time() * 1000)
  390. def order_request_to_order_wire(order: OrderRequest, asset: int) -> OrderWire:
  391. order_wire: OrderWire = {
  392. "a": asset,
  393. "b": order["is_buy"],
  394. "p": float_to_wire(order["limit_px"]),
  395. "s": float_to_wire(order["sz"]),
  396. "r": order["reduce_only"],
  397. "t": order_type_to_wire(order["order_type"]),
  398. }
  399. if "cloid" in order and order["cloid"] is not None:
  400. order_wire["c"] = order["cloid"].to_raw()
  401. return order_wire
  402. def order_wires_to_order_action(order_wires, builder=None):
  403. action = {
  404. "type": "order",
  405. "orders": order_wires,
  406. "grouping": "na",
  407. }
  408. if builder:
  409. action["builder"] = builder
  410. return action