diff --git a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_auth.py b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_auth.py index 7d99850eef..3447a23ecb 100644 --- a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_auth.py +++ b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_auth.py @@ -26,9 +26,10 @@ class HyperliquidPerpetualAuth(AuthBase): Auth class required by Hyperliquid Perpetual API """ - def __init__(self, api_key: str, api_secret: str): + def __init__(self, api_key: str, api_secret: str, use_vault: bool): self._api_key: str = api_key self._api_secret: str = api_secret + self._use_vault: bool = use_vault self.wallet = eth_account.Account.from_key(api_secret) def sign_inner(self, wallet, data): @@ -95,7 +96,7 @@ def _sign_update_leverage_params(self, params, base_url, timestamp): self.wallet, signature_types, res, - ZERO_ADDRESS, + ZERO_ADDRESS if not self._use_vault else self._api_key, timestamp, CONSTANTS.PERPETUAL_BASE_URL in base_url, ) @@ -103,7 +104,7 @@ def _sign_update_leverage_params(self, params, base_url, timestamp): "action": params, "nonce": timestamp, "signature": signature, - "vaultAddress": None, + "vaultAddress": self._api_key if self._use_vault else None, } return payload @@ -119,7 +120,7 @@ def _sign_cancel_params(self, params, base_url, timestamp): self.wallet, signature_types, [[res]], - ZERO_ADDRESS, + ZERO_ADDRESS if not self._use_vault else self._api_key, timestamp, CONSTANTS.PERPETUAL_BASE_URL in base_url, ) @@ -130,7 +131,8 @@ def _sign_cancel_params(self, params, base_url, timestamp): }, "nonce": timestamp, "signature": signature, - "vaultAddress": None, + "vaultAddress": self._api_key if self._use_vault else None, + } return payload @@ -155,7 +157,7 @@ def _sign_order_params(self, params, base_url, timestamp): self.wallet, signature_types, [[res], order_grouping_to_number(grouping)], - ZERO_ADDRESS, + ZERO_ADDRESS if not self._use_vault else self._api_key, timestamp, CONSTANTS.PERPETUAL_BASE_URL in base_url, ) @@ -168,7 +170,8 @@ def _sign_order_params(self, params, base_url, timestamp): }, "nonce": timestamp, "signature": signature, - "vaultAddress": None, + "vaultAddress": self._api_key if self._use_vault else None, + } return payload diff --git a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_derivative.py b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_derivative.py index 9be7461aee..ca07fbea4e 100644 --- a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_derivative.py +++ b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_derivative.py @@ -40,20 +40,23 @@ class HyperliquidPerpetualDerivative(PerpetualDerivativePyBase): web_utils = web_utils + SHORT_POLL_INTERVAL = 5.0 LONG_POLL_INTERVAL = 12.0 def __init__( self, client_config_map: "ClientConfigAdapter", - hyperliquid_perpetual_api_key: str = None, hyperliquid_perpetual_api_secret: str = None, + use_vault: bool = False, + hyperliquid_perpetual_api_key: str = None, trading_pairs: Optional[List[str]] = None, trading_required: bool = True, domain: str = CONSTANTS.DOMAIN, ): self.hyperliquid_perpetual_api_key = hyperliquid_perpetual_api_key self.hyperliquid_perpetual_secret_key = hyperliquid_perpetual_api_secret + self._use_vault = use_vault self._trading_required = trading_required self._trading_pairs = trading_pairs self._domain = domain @@ -62,6 +65,10 @@ def __init__( self.coin_to_asset: Dict[str, int] = {} super().__init__(client_config_map) + SHORT_POLL_INTERVAL = 5.0 + + LONG_POLL_INTERVAL = 12.0 + @property def name(self) -> str: # Note: domain here refers to the entire exchange name. i.e. hyperliquid_perpetual or hyperliquid_perpetual_testnet @@ -69,7 +76,8 @@ def name(self) -> str: @property def authenticator(self) -> HyperliquidPerpetualAuth: - return HyperliquidPerpetualAuth(self.hyperliquid_perpetual_api_key, self.hyperliquid_perpetual_secret_key) + return HyperliquidPerpetualAuth(self.hyperliquid_perpetual_api_key, self.hyperliquid_perpetual_secret_key, + self._use_vault) @property def rate_limits_rules(self) -> List[RateLimit]: @@ -464,7 +472,8 @@ async def _handle_update_error_for_active_order(self, order: InFlightOrder, erro self.logger().warning( f"Error fetching status update for the active order {order.client_order_id}: {request_error}.", ) - self.logger().debug(f"Order {order.client_order_id} not found counter: {self._order_tracker._order_not_found_records.get(order.client_order_id, 0)}") + self.logger().debug( + f"Order {order.client_order_id} not found counter: {self._order_tracker._order_not_found_records.get(order.client_order_id, 0)}") await self._order_tracker.process_order_not_found(order.client_order_id) async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpdate: diff --git a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_utils.py b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_utils.py index c18b62a994..d72d554182 100644 --- a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_utils.py +++ b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_utils.py @@ -1,6 +1,8 @@ from decimal import Decimal +from typing import Optional from pydantic import Field, SecretStr +from pydantic.class_validators import validator from hummingbot.client.config.config_data_types import BaseConnectorConfigMap, ClientFieldData from hummingbot.core.data_type.trade_fee import TradeFeeSchema @@ -19,27 +21,54 @@ BROKER_ID = "HBOT" +def validate_bool(value: str) -> Optional[str]: + """ + Permissively interpret a string as a boolean + """ + valid_values = ('true', 'yes', 'y', 'false', 'no', 'n') + if value.lower() not in valid_values: + return f"Invalid value, please choose value from {valid_values}" + + class HyperliquidPerpetualConfigMap(BaseConnectorConfigMap): connector: str = Field(default="hyperliquid_perpetual", client_data=None) - hyperliquid_perpetual_api_key: SecretStr = Field( + hyperliquid_perpetual_api_secret: SecretStr = Field( default=..., client_data=ClientFieldData( - prompt=lambda cm: "Enter your Arbitrum wallet public key", + prompt=lambda cm: "Enter your Arbitrum wallet private key", is_secure=True, is_connect_key=True, prompt_on_new=True, ) ) - hyperliquid_perpetual_api_secret: SecretStr = Field( + use_vault: bool = Field( + default="no", + client_data=ClientFieldData( + prompt=lambda cm: "Do you want to use the vault address?(Yes/No)", + is_secure=False, + is_connect_key=True, + prompt_on_new=True, + ), + ) + hyperliquid_perpetual_api_key: SecretStr = Field( default=..., client_data=ClientFieldData( - prompt=lambda cm: "Enter your Arbitrum wallet private key", + prompt=lambda cm: "Enter your Arbitrum or vault address", is_secure=True, is_connect_key=True, prompt_on_new=True, ) ) + @validator("use_vault", pre=True) + def validate_bool(cls, v: str): + """Used for client-friendly error output.""" + if isinstance(v, str): + ret = validate_bool(v) + if ret is not None: + raise ValueError(ret) + return v + KEYS = HyperliquidPerpetualConfigMap.construct() @@ -51,19 +80,28 @@ class HyperliquidPerpetualConfigMap(BaseConnectorConfigMap): class HyperliquidPerpetualTestnetConfigMap(BaseConnectorConfigMap): connector: str = Field(default="hyperliquid_perpetual_testnet", client_data=None) - hyperliquid_perpetual_testnet_api_key: SecretStr = Field( + hyperliquid_perpetual_testnet_api_secret: SecretStr = Field( default=..., client_data=ClientFieldData( - prompt=lambda cm: "Enter your Arbitrum wallet address", + prompt=lambda cm: "Enter your Arbitrum wallet private key", is_secure=True, is_connect_key=True, prompt_on_new=True, ) ) - hyperliquid_perpetual_testnet_api_secret: SecretStr = Field( + use_vault: bool = Field( + default="no", + client_data=ClientFieldData( + prompt=lambda cm: "Do you want to use the vault address?(Yes/No)", + is_secure=False, + is_connect_key=True, + prompt_on_new=True, + ), + ) + hyperliquid_perpetual_testnet_api_key: SecretStr = Field( default=..., client_data=ClientFieldData( - prompt=lambda cm: "Enter your Arbitrum wallet private key", + prompt=lambda cm: "Enter your Arbitrum or vault address", is_secure=True, is_connect_key=True, prompt_on_new=True, @@ -73,5 +111,14 @@ class HyperliquidPerpetualTestnetConfigMap(BaseConnectorConfigMap): class Config: title = "hyperliquid_perpetual" + @validator("use_vault", pre=True) + def validate_bool(cls, v: str): + """Used for client-friendly error output.""" + if isinstance(v, str): + ret = validate_bool(v) + if ret is not None: + raise ValueError(ret) + return v + OTHER_DOMAINS_KEYS = {"hyperliquid_perpetual_testnet": HyperliquidPerpetualTestnetConfigMap.construct()} diff --git a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_api_order_book_data_source.py b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_api_order_book_data_source.py index 9c1f061ca9..c28705256c 100644 --- a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_api_order_book_data_source.py +++ b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_api_order_book_data_source.py @@ -49,6 +49,7 @@ def setUp(self) -> None: client_config_map, hyperliquid_perpetual_api_key="testkey", hyperliquid_perpetual_api_secret="13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930", # noqa: mock + use_vault=False, trading_pairs=[self.trading_pair], ) self.data_source = HyperliquidPerpetualAPIOrderBookDataSource( diff --git a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_auth.py b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_auth.py index afd52d3832..798821a17d 100644 --- a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_auth.py +++ b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_auth.py @@ -13,8 +13,8 @@ def setUp(self) -> None: super().setUp() self.api_key = "testApiKey" self.secret_key = "13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930" # noqa: mock - - self.auth = HyperliquidPerpetualAuth(api_key=self.api_key, api_secret=self.secret_key) + self.use_vault = False # noqa: mock + self.auth = HyperliquidPerpetualAuth(api_key=self.api_key, api_secret=self.secret_key, use_vault=self.use_vault) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = asyncio.get_event_loop().run_until_complete(asyncio.wait_for(coroutine, timeout)) diff --git a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_derivative.py b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_derivative.py index dec865ef0a..6e1bdefc22 100644 --- a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_derivative.py +++ b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_derivative.py @@ -36,6 +36,7 @@ def setUpClass(cls) -> None: super().setUpClass() cls.api_key = "someKey" cls.api_secret = "13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930" # noqa: mock + cls.use_vault = False # noqa: mock cls.user_id = "someUserId" cls.base_asset = "BTC" cls.quote_asset = "USD" # linear @@ -385,8 +386,9 @@ def create_exchange_instance(self): client_config_map = ClientConfigAdapter(ClientConfigMap()) exchange = HyperliquidPerpetualDerivative( client_config_map, - self.api_key, self.api_secret, + self.use_vault, + self.api_key, trading_pairs=[self.trading_pair], ) # exchange._last_trade_history_timestamp = self.latest_trade_hist_timestamp @@ -777,6 +779,7 @@ def test_supported_position_modes(self): client_config_map=client_config_map, hyperliquid_perpetual_api_key=self.api_key, hyperliquid_perpetual_api_secret=self.api_secret, + use_vault=self.use_vault, trading_pairs=[self.trading_pair], ) diff --git a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_user_stream_data_source.py b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_user_stream_data_source.py index 261620e04f..46a3589137 100644 --- a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_user_stream_data_source.py +++ b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_user_stream_data_source.py @@ -34,6 +34,7 @@ def setUpClass(cls) -> None: cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}" cls.api_key = "someKey" + cls.use_vault = False cls.api_secret_key = "13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930" # noqa: mock" def setUp(self) -> None: @@ -47,7 +48,8 @@ def setUp(self) -> None: self.mock_time_provider.time.return_value = 1000 self.auth = HyperliquidPerpetualAuth( api_key=self.api_key, - api_secret=self.api_secret_key) + api_secret=self.api_secret_key, + use_vault=self.use_vault) self.time_synchronizer = TimeSynchronizer() self.time_synchronizer.add_time_offset_ms_sample(0) @@ -56,6 +58,7 @@ def setUp(self) -> None: client_config_map=client_config_map, hyperliquid_perpetual_api_key=self.api_key, hyperliquid_perpetual_api_secret=self.api_secret_key, + use_vault=self.use_vault, trading_pairs=[]) self.connector._web_assistants_factory._auth = self.auth diff --git a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_utils.py b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_utils.py index 66f9fd489b..851be7b65f 100644 --- a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_utils.py +++ b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_utils.py @@ -1,5 +1,53 @@ from unittest import TestCase +from hummingbot.connector.derivative.hyperliquid_perpetual.hyperliquid_perpetual_utils import ( + HyperliquidPerpetualConfigMap, + HyperliquidPerpetualTestnetConfigMap, + validate_bool, +) + class HyperliquidPerpetualUtilsTests(TestCase): pass + + def test_validate_bool_succeed(self): + valid_values = ['true', 'yes', 'y', 'false', 'no', 'n'] + + validations = [validate_bool(value) for value in valid_values] + for validation in validations: + self.assertIsNone(validation) + + def test_validate_bool_fails(self): + wrong_value = "ye" + valid_values = ('true', 'yes', 'y', 'false', 'no', 'n') + + validation_error = validate_bool(wrong_value) + self.assertEqual(validation_error, f"Invalid value, please choose value from {valid_values}") + + def test_cls_validate_bool_succeed(self): + valid_values = ['true', 'yes', 'y', 'false', 'no', 'n'] + + validations = [HyperliquidPerpetualConfigMap.validate_bool(value) for value in valid_values] + for validation in validations: + self.assertTrue(validation) + + def test_cls_validate_bool_fails(self): + wrong_value = "ye" + valid_values = ('true', 'yes', 'y', 'false', 'no', 'n') + with self.assertRaises(ValueError) as exception_context: + HyperliquidPerpetualConfigMap.validate_bool(wrong_value) + self.assertEqual(str(exception_context.exception), f"Invalid value, please choose value from {valid_values}") + + def test_cls_testnet_validate_bool_succeed(self): + valid_values = ['true', 'yes', 'y', 'false', 'no', 'n'] + + validations = [HyperliquidPerpetualTestnetConfigMap.validate_bool(value) for value in valid_values] + for validation in validations: + self.assertTrue(validation) + + def test_cls_testnet_validate_bool_fails(self): + wrong_value = "ye" + valid_values = ('true', 'yes', 'y', 'false', 'no', 'n') + with self.assertRaises(ValueError) as exception_context: + HyperliquidPerpetualTestnetConfigMap.validate_bool(wrong_value) + self.assertEqual(str(exception_context.exception), f"Invalid value, please choose value from {valid_values}")