import time from decimal import Decimal import msgpack from eth_account import Account from eth_account.messages import encode_typed_data from eth_utils import keccak, to_hex from hyperliquid.utils.types import Cloid, Literal, NotRequired, Optional, TypedDict, Union Tif = Union[Literal["Alo"], Literal["Ioc"], Literal["Gtc"]] Tpsl = Union[Literal["tp"], Literal["sl"]] LimitOrderType = TypedDict("LimitOrderType", {"tif": Tif}) TriggerOrderType = TypedDict("TriggerOrderType", {"triggerPx": float, "isMarket": bool, "tpsl": Tpsl}) TriggerOrderTypeWire = TypedDict("TriggerOrderTypeWire", {"triggerPx": str, "isMarket": bool, "tpsl": Tpsl}) OrderType = TypedDict("OrderType", {"limit": LimitOrderType, "trigger": TriggerOrderType}, total=False) OrderTypeWire = TypedDict("OrderTypeWire", {"limit": LimitOrderType, "trigger": TriggerOrderTypeWire}, total=False) OrderRequest = TypedDict( "OrderRequest", { "coin": str, "is_buy": bool, "sz": float, "limit_px": float, "order_type": OrderType, "reduce_only": bool, "cloid": NotRequired[Optional[Cloid]], }, total=False, ) OidOrCloid = Union[int, Cloid] ModifyRequest = TypedDict( "ModifyRequest", { "oid": OidOrCloid, "order": OrderRequest, }, total=False, ) CancelRequest = TypedDict("CancelRequest", {"coin": str, "oid": int}) CancelByCloidRequest = TypedDict("CancelByCloidRequest", {"coin": str, "cloid": Cloid}) Grouping = Union[Literal["na"], Literal["normalTpsl"], Literal["positionTpsl"]] Order = TypedDict( "Order", {"asset": int, "isBuy": bool, "limitPx": float, "sz": float, "reduceOnly": bool, "cloid": Optional[Cloid]} ) OrderWire = TypedDict( "OrderWire", { "a": int, "b": bool, "p": str, "s": str, "r": bool, "t": OrderTypeWire, "c": NotRequired[Optional[str]], }, ) ModifyWire = TypedDict( "ModifyWire", { "oid": int, "order": OrderWire, }, ) ScheduleCancelAction = TypedDict( "ScheduleCancelAction", { "type": Literal["scheduleCancel"], "time": NotRequired[Optional[int]], }, ) USD_SEND_SIGN_TYPES = [ {"name": "hyperliquidChain", "type": "string"}, {"name": "destination", "type": "string"}, {"name": "amount", "type": "string"}, {"name": "time", "type": "uint64"}, ] SPOT_TRANSFER_SIGN_TYPES = [ {"name": "hyperliquidChain", "type": "string"}, {"name": "destination", "type": "string"}, {"name": "token", "type": "string"}, {"name": "amount", "type": "string"}, {"name": "time", "type": "uint64"}, ] WITHDRAW_SIGN_TYPES = [ {"name": "hyperliquidChain", "type": "string"}, {"name": "destination", "type": "string"}, {"name": "amount", "type": "string"}, {"name": "time", "type": "uint64"}, ] USD_CLASS_TRANSFER_SIGN_TYPES = [ {"name": "hyperliquidChain", "type": "string"}, {"name": "amount", "type": "string"}, {"name": "toPerp", "type": "bool"}, {"name": "nonce", "type": "uint64"}, ] SEND_ASSET_SIGN_TYPES = [ {"name": "hyperliquidChain", "type": "string"}, {"name": "destination", "type": "string"}, {"name": "sourceDex", "type": "string"}, {"name": "destinationDex", "type": "string"}, {"name": "token", "type": "string"}, {"name": "amount", "type": "string"}, {"name": "fromSubAccount", "type": "string"}, {"name": "nonce", "type": "uint64"}, ] TOKEN_DELEGATE_TYPES = [ {"name": "hyperliquidChain", "type": "string"}, {"name": "validator", "type": "address"}, {"name": "wei", "type": "uint64"}, {"name": "isUndelegate", "type": "bool"}, {"name": "nonce", "type": "uint64"}, ] CONVERT_TO_MULTI_SIG_USER_SIGN_TYPES = [ {"name": "hyperliquidChain", "type": "string"}, {"name": "signers", "type": "string"}, {"name": "nonce", "type": "uint64"}, ] MULTI_SIG_ENVELOPE_SIGN_TYPES = [ {"name": "hyperliquidChain", "type": "string"}, {"name": "multiSigActionHash", "type": "bytes32"}, {"name": "nonce", "type": "uint64"}, ] def order_type_to_wire(order_type: OrderType) -> OrderTypeWire: if "limit" in order_type: return {"limit": order_type["limit"]} elif "trigger" in order_type: return { "trigger": { "isMarket": order_type["trigger"]["isMarket"], "triggerPx": float_to_wire(order_type["trigger"]["triggerPx"]), "tpsl": order_type["trigger"]["tpsl"], } } raise ValueError("Invalid order type", order_type) def address_to_bytes(address): return bytes.fromhex(address[2:] if address.startswith("0x") else address) def action_hash(action, vault_address, nonce, expires_after): data = msgpack.packb(action) data += nonce.to_bytes(8, "big") if vault_address is None: data += b"\x00" else: data += b"\x01" data += address_to_bytes(vault_address) if expires_after is not None: data += b"\x00" data += expires_after.to_bytes(8, "big") return keccak(data) def construct_phantom_agent(hash, is_mainnet): return {"source": "a" if is_mainnet else "b", "connectionId": hash} def l1_payload(phantom_agent): return { "domain": { "chainId": 1337, "name": "Exchange", "verifyingContract": "0x0000000000000000000000000000000000000000", "version": "1", }, "types": { "Agent": [ {"name": "source", "type": "string"}, {"name": "connectionId", "type": "bytes32"}, ], "EIP712Domain": [ {"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"}, ], }, "primaryType": "Agent", "message": phantom_agent, } def user_signed_payload(primary_type, payload_types, action): chain_id = int(action["signatureChainId"], 16) return { "domain": { "name": "HyperliquidSignTransaction", "version": "1", "chainId": chain_id, "verifyingContract": "0x0000000000000000000000000000000000000000", }, "types": { primary_type: payload_types, "EIP712Domain": [ {"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"}, ], }, "primaryType": primary_type, "message": action, } def sign_l1_action(wallet, action, active_pool, nonce, expires_after, is_mainnet): hash = action_hash(action, active_pool, nonce, expires_after) phantom_agent = construct_phantom_agent(hash, is_mainnet) data = l1_payload(phantom_agent) return sign_inner(wallet, data) def sign_user_signed_action(wallet, action, payload_types, primary_type, is_mainnet): # signatureChainId is the chain used by the wallet to sign and can be any chain. # hyperliquidChain determines the environment and prevents replaying an action on a different chain. action["signatureChainId"] = "0x66eee" action["hyperliquidChain"] = "Mainnet" if is_mainnet else "Testnet" data = user_signed_payload(primary_type, payload_types, action) return sign_inner(wallet, data) def add_multi_sig_types(sign_types): enriched_sign_types = [] enriched = False for sign_type in sign_types: enriched_sign_types.append(sign_type) if sign_type["name"] == "hyperliquidChain": enriched = True enriched_sign_types.append( { "name": "payloadMultiSigUser", "type": "address", } ) enriched_sign_types.append( { "name": "outerSigner", "type": "address", } ) if not enriched: print('"hyperliquidChain" missing from sign_types. sign_types was not enriched with multi-sig signing types') return enriched_sign_types def add_multi_sig_fields(action, payload_multi_sig_user, outer_signer): action = action.copy() action["payloadMultiSigUser"] = payload_multi_sig_user.lower() action["outerSigner"] = outer_signer.lower() return action def sign_multi_sig_user_signed_action_payload( wallet, action, is_mainnet, sign_types, tx_type, payload_multi_sig_user, outer_signer ): envelope = add_multi_sig_fields(action, payload_multi_sig_user, outer_signer) sign_types = add_multi_sig_types(sign_types) return sign_user_signed_action( wallet, envelope, sign_types, tx_type, is_mainnet, ) def sign_multi_sig_l1_action_payload( wallet, action, is_mainnet, vault_address, timestamp, expires_after, payload_multi_sig_user, outer_signer ): envelope = [payload_multi_sig_user.lower(), outer_signer.lower(), action] return sign_l1_action( wallet, envelope, vault_address, timestamp, expires_after, is_mainnet, ) def sign_multi_sig_action(wallet, action, is_mainnet, vault_address, nonce, expires_after): action_without_tag = action.copy() del action_without_tag["type"] multi_sig_action_hash = action_hash(action_without_tag, vault_address, nonce, expires_after) envelope = { "multiSigActionHash": multi_sig_action_hash, "nonce": nonce, } return sign_user_signed_action( wallet, envelope, MULTI_SIG_ENVELOPE_SIGN_TYPES, "HyperliquidTransaction:SendMultiSig", is_mainnet, ) def sign_usd_transfer_action(wallet, action, is_mainnet): return sign_user_signed_action( wallet, action, USD_SEND_SIGN_TYPES, "HyperliquidTransaction:UsdSend", is_mainnet, ) def sign_spot_transfer_action(wallet, action, is_mainnet): return sign_user_signed_action( wallet, action, SPOT_TRANSFER_SIGN_TYPES, "HyperliquidTransaction:SpotSend", is_mainnet, ) def sign_withdraw_from_bridge_action(wallet, action, is_mainnet): return sign_user_signed_action( wallet, action, WITHDRAW_SIGN_TYPES, "HyperliquidTransaction:Withdraw", is_mainnet, ) def sign_usd_class_transfer_action(wallet, action, is_mainnet): return sign_user_signed_action( wallet, action, USD_CLASS_TRANSFER_SIGN_TYPES, "HyperliquidTransaction:UsdClassTransfer", is_mainnet, ) def sign_send_asset_action(wallet, action, is_mainnet): return sign_user_signed_action( wallet, action, SEND_ASSET_SIGN_TYPES, "HyperliquidTransaction:SendAsset", is_mainnet, ) def sign_convert_to_multi_sig_user_action(wallet, action, is_mainnet): return sign_user_signed_action( wallet, action, CONVERT_TO_MULTI_SIG_USER_SIGN_TYPES, "HyperliquidTransaction:ConvertToMultiSigUser", is_mainnet, ) def sign_agent(wallet, action, is_mainnet): return sign_user_signed_action( wallet, action, [ {"name": "hyperliquidChain", "type": "string"}, {"name": "agentAddress", "type": "address"}, {"name": "agentName", "type": "string"}, {"name": "nonce", "type": "uint64"}, ], "HyperliquidTransaction:ApproveAgent", is_mainnet, ) def sign_approve_builder_fee(wallet, action, is_mainnet): return sign_user_signed_action( wallet, action, [ {"name": "hyperliquidChain", "type": "string"}, {"name": "maxFeeRate", "type": "string"}, {"name": "builder", "type": "address"}, {"name": "nonce", "type": "uint64"}, ], "HyperliquidTransaction:ApproveBuilderFee", is_mainnet, ) def sign_token_delegate_action(wallet, action, is_mainnet): return sign_user_signed_action( wallet, action, TOKEN_DELEGATE_TYPES, "HyperliquidTransaction:TokenDelegate", is_mainnet, ) def sign_inner(wallet, data): structured_data = encode_typed_data(full_message=data) signed = wallet.sign_message(structured_data) return {"r": to_hex(signed["r"]), "s": to_hex(signed["s"]), "v": signed["v"]} def recover_agent_or_user_from_l1_action(action, signature, active_pool, nonce, expires_after, is_mainnet): hash = action_hash(action, active_pool, nonce, expires_after) phantom_agent = construct_phantom_agent(hash, is_mainnet) data = l1_payload(phantom_agent) structured_data = encode_typed_data(full_message=data) address = Account.recover_message(structured_data, vrs=[signature["v"], signature["r"], signature["s"]]) return address def recover_user_from_user_signed_action(action, signature, payload_types, primary_type, is_mainnet): action["hyperliquidChain"] = "Mainnet" if is_mainnet else "Testnet" data = user_signed_payload(primary_type, payload_types, action) structured_data = encode_typed_data(full_message=data) address = Account.recover_message(structured_data, vrs=[signature["v"], signature["r"], signature["s"]]) return address def float_to_wire(x: float) -> str: rounded = f"{x:.8f}" if abs(float(rounded) - x) >= 1e-12: raise ValueError("float_to_wire causes rounding", x) if rounded == "-0": rounded = "0" normalized = Decimal(rounded).normalize() return f"{normalized:f}" def float_to_int_for_hashing(x: float) -> int: return float_to_int(x, 8) def float_to_usd_int(x: float) -> int: return float_to_int(x, 6) def float_to_int(x: float, power: int) -> int: with_decimals = x * 10**power if abs(round(with_decimals) - with_decimals) >= 1e-3: raise ValueError("float_to_int causes rounding", x) res: int = round(with_decimals) return res def get_timestamp_ms() -> int: return int(time.time() * 1000) def order_request_to_order_wire(order: OrderRequest, asset: int) -> OrderWire: order_wire: OrderWire = { "a": asset, "b": order["is_buy"], "p": float_to_wire(order["limit_px"]), "s": float_to_wire(order["sz"]), "r": order["reduce_only"], "t": order_type_to_wire(order["order_type"]), } if "cloid" in order and order["cloid"] is not None: order_wire["c"] = order["cloid"].to_raw() return order_wire def order_wires_to_order_action(order_wires, builder=None): action = { "type": "order", "orders": order_wires, "grouping": "na", } if builder: action["builder"] = builder return action