# ruff: noqa: D200 # ruff: noqa: D204 # ruff: noqa: D205 # ruff: noqa: D404 # ruff: noqa: W291 # ruff: noqa: D400 # ruff: noqa: E501 import json import logging import random import time from dataclasses import dataclass from datetime import datetime from decimal import Decimal from enum import Enum from http.cookies import SimpleCookie from typing import Any import aiohttp import requests from eth_account import Account from eth_account.messages import encode_typed_data, SignableMessage from .grvt_ccxt_env import CHAIN_IDS, GrvtEnv from .grvt_ccxt_types import ( BTC_ETH_SIZE_MULTIPLIER, DURATION_SECOND_IN_NSEC, Amount, GrvtOrderSide, GrvtOrderType, Num, ) def rand_uint32(): return random.randint(0, 2**32 - 1) class TimeInForce(Enum): """ | | Must Fill All | Can Fill Partial | | - | - | - | | Must Fill Immediately | FOK | IOC | | Can Fill Till Time | AON | GTC |. """ # GTT - Remains open until it is cancelled, or expired GOOD_TILL_TIME = "GOOD_TILL_TIME" # AON - Either fill the whole order or none of it (Block Trades Only) ALL_OR_NONE = "ALL_OR_NONE" # IOC - Fill the order as much as possible, when hitting the orderbook. Then cancel it IMMEDIATE_OR_CANCEL = "IMMEDIATE_OR_CANCEL" # FOK - Both AoN and IoC. Either fill the full order when hitting the orderbook, or cancel it FILL_OR_KILL = "FILL_OR_KILL" class SignTimeInForce(Enum): GOOD_TILL_TIME = 1 ALL_OR_NONE = 2 IMMEDIATE_OR_CANCEL = 3 FILL_OR_KILL = 4 TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE = { TimeInForce.GOOD_TILL_TIME: SignTimeInForce.GOOD_TILL_TIME, TimeInForce.ALL_OR_NONE: SignTimeInForce.ALL_OR_NONE, TimeInForce.IMMEDIATE_OR_CANCEL: SignTimeInForce.IMMEDIATE_OR_CANCEL, TimeInForce.FILL_OR_KILL: SignTimeInForce.FILL_OR_KILL, } def get_EIP712_domain_data(env: GrvtEnv) -> dict[str, str | int]: # DO NOT MODIFY THESE VALUES ############## return { "name": "GRVT Exchange", "version": "0", "chainId": CHAIN_IDS[env.value], } def get_cookie_with_expiration( path: str, api_key: str | None ) -> dict[str, str | float | None] | None: """ Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token. :return: The session cookie. """ FN = f"get_cookie_with_expiration {path=}" if api_key: data = {} try: data = {"api_key": api_key} session = requests.Session() return_value = session.post( path, json=data, headers={"Content-Type": "application/json"}, timeout=5, ) if return_value.ok: cookie = SimpleCookie() cookie.load(return_value.headers.get("Set-Cookie", "")) cookie_value: str = cookie["gravity"].value cookie_expiry: datetime = datetime.strptime( cookie["gravity"]["expires"], "%a, %d %b %Y %H:%M:%S %Z", ) grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "") logging.info( f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}" ) return { "gravity": cookie_value, "expires": cookie_expiry.timestamp(), "X-Grvt-Account-Id": grvt_account_id, } logging.warning(f"{FN} Invalid return_value {data=} {path=} {return_value=}") return None except Exception as e: logging.error(f"{FN} Error getting cookie: {e}") return None else: return None async def get_cookie_with_expiration_async( path: str, api_key: str | None ) -> dict[str, str | float | None] | None: """ Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token. :return: The session cookie. """ FN = f"get_cookie_with_expiration_async {path=}" if api_key: data = {} try: data = {"api_key": api_key} logging.info(f"{FN} ask for cookie {path=} {data=}") async with aiohttp.ClientSession() as session: async with session.post(url=path, json=data, timeout=5) as return_value: logging.info(f"{FN} {return_value=}") if return_value.ok: cookie = SimpleCookie() cookie.load(return_value.headers.get("Set-Cookie", "")) cookie_value: str = cookie["gravity"].value cookie_expiry: datetime = datetime.strptime( cookie["gravity"]["expires"], "%a, %d %b %Y %H:%M:%S %Z", ) grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "") logging.info( f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}" ) return { "gravity": cookie_value, "expires": cookie_expiry.timestamp(), "X-Grvt-Account-Id": grvt_account_id, } except Exception as e: logging.error(f"{FN} Error getting cookie: {e}") return None else: return None class GrvtKind(Enum): PERPETUAL = 1 FUTURE = 2 CALL = 3 PUT = 4 SPOT = 5 class GrvtCurrency(Enum): USD = 1 USDC = 2 USDT = 3 ETH = 4 BTC = 5 def hexlify(data: bytes) -> str: """Convert a byte array to a hex string with a 0x prefix.""" return f"0x{data.hex()}" class EnumEncoder(json.JSONEncoder): def default(self, o): """ Custom JSON encoder for Enum types. :param obj: Object to serialize. :return: Serialized object. """ if isinstance(o, Enum): return o.value return super().default(o) def get_kuq_from_symbol(symbol: str) -> tuple[str, str, str]: parts = symbol.split("_") if len(parts) == 3: underlying, quote, kind = parts if kind == "Perp": kind = "PERPETUAL" else: raise ValueError(f"Invalid {symbol=} {kind=}") elif len(parts) == 4: underlying, quote, kind, time_str = parts if kind == "Fut": kind = "FUTURE" else: raise ValueError(f"Invalid {symbol=} {kind=}") elif len(parts) == 5: underlying, quote, kind, time_str, strike_price = parts if kind in {"Call", "Put"}: kind = kind.upper() else: raise ValueError(f"Invalid {symbol=} {kind=}") else: raise ValueError(f"Invalid {symbol=}") return kind, underlying, quote # Custom types EIP712_ORDER_MESSAGE_TYPE = { "Order": [ {"name": "subAccountID", "type": "uint64"}, {"name": "isMarket", "type": "bool"}, {"name": "timeInForce", "type": "uint8"}, {"name": "postOnly", "type": "bool"}, {"name": "reduceOnly", "type": "bool"}, {"name": "legs", "type": "OrderLeg[]"}, {"name": "nonce", "type": "uint32"}, {"name": "expiration", "type": "int64"}, ], "OrderLeg": [ {"name": "assetID", "type": "uint256"}, {"name": "contractSize", "type": "uint64"}, {"name": "limitPrice", "type": "uint64"}, {"name": "isBuyingContract", "type": "bool"}, ], } @dataclass class GrvtSignature: # The address (public key) of the wallet signing the payload signer: str r: str s: str v: int # Timestamp after which this signature expires, expressed in unix nanoseconds. # Must be capped at 30 days expiration: str """ Users can randomly generate this value, used as a signature deconflicting key. ie. You can send the same exact instruction twice with different nonces. When the same nonce is used, the same payload will generate the same signature. Our system will consider the payload a duplicate, and ignore it. """ nonce: int @dataclass class OrderMetadata: """ Metadata fields are used to support Backend only operations. Hence, fields in here are never signed, and is never transmitted to the smart contract. """ """ `client_order_id`: A unique identifier of an active order, specified by the client This is used to identify the order in the client's system This value must be unique for all active orders in a subaccount, otehrwise amendment / cancellation will not work as expected Gravity UI will generate a random clientOrderID for each order in the range [0, 2^63 - 1] To prevent any conflicts, client machines should generate a random clientOrderID in the range [2^63, 2^64 - 1]. When GRVT Backend receives an order with duplicate `client_order_id`, it will reject the order with rejectReason set to duplicate `client_order_id`. """ client_order_id: str # [Filled by GRVT Backend] Time at which the order was received by GRVT in unix nanoseconds create_time: str | None = None @dataclass class GrvtOrderLeg: # The instrument to trade in this leg instrument: str # The total number of contracts to trade in this leg, expressed in base currency units. size: Decimal # Specifies if the order leg is a buy or sell is_buying_asset: bool """ The limit price of the order leg, expressed in `9` decimals. This is the number of quote currency units to pay/receive for this leg. This should be `null/0` if the order is a market order """ limit_price: Decimal @dataclass class GrvtOrder: """ Order is a typed payload used throughout the GRVT platform to express all orders. GRVT orders are capable of expressing both single-legged, and multi-legged orders by default. All fields in the Order payload (except `id`, `metadata`, and `state`) are trustlessly enforced on our Hyperchain. This minimizes the amount of trust users have to offer to GRVT. """ # The subaccount initiating the order sub_account_id: str # Supported time_in_force : GTT, IOC, FOK: