Skip to content

Commit 0d456d0

Browse files
committed
prod/test env
1 parent ff4a397 commit 0d456d0

9 files changed

Lines changed: 125 additions & 60 deletions

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,24 @@ python -m hypertrade
7474

7575
Hypertrade won't start unless these variables are set:
7676

77-
- `HYPERTRADE_MASTER_ADDR`
78-
- `HYPERTRADE_API_WALLET_PRIV`
79-
- `HYPERTRADE_SUBACCOUNT_ADDR`
77+
- `HYPERTRADE_ENVIRONMENT` – The Hyperliquid environment to connect to. Must be `prod` or `test`.
78+
- `HYPERTRADE_MASTER_ADDR` – Your master wallet address.
79+
- `HYPERTRADE_API_WALLET_PRIV` – Your private key for API access.
80+
- `HYPERTRADE_SUBACCOUNT_ADDR` – (Optional) Your Hyperliquid sub-account address.
81+
82+
### API Endpoint Selection
83+
84+
The `HYPERTRADE_ENVIRONMENT` variable controls which Hyperliquid endpoint is used:
85+
86+
- `HYPERTRADE_ENVIRONMENT=prod``https://api.hyperliquid.xyz` (mainnet)
87+
- `HYPERTRADE_ENVIRONMENT=test``https://api.hyperliquid-testnet.xyz` (testnet)
88+
89+
Any other value will cause the application to fail at startup with a clear error message.
90+
91+
### Example Setup
8092

8193
```bash
94+
export HYPERTRADE_ENVIRONMENT=prod
8295
export HYPERTRADE_MASTER_ADDR=0xYourMasterAddress
8396
export HYPERTRADE_API_WALLET_PRIV='your-private-key'
8497
export HYPERTRADE_SUBACCOUNT_ADDR=0xYourSubaccountAddress
@@ -91,6 +104,7 @@ Personally I prefer storing secrets with Password Store (pass) instead of a `.en
91104
- Example exports pulling from pass:
92105

93106
```bash
107+
export HYPERTRADE_ENVIRONMENT=prod
94108
export HYPERTRADE_MASTER_ADDR="$(pass show hypertrade/master_addr)"
95109
export HYPERTRADE_API_WALLET_PRIV="$(pass show hypertrade/api_wallet_priv)"
96110
export HYPERTRADE_SUBACCOUNT_ADDR="$(pass show hypertrade/subaccount_addr)"

hypertrade/config.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ class Settings(BaseSettings):
1212

1313
# ── Core ─────────────────────────────────────
1414
app_name: str = "Hypertrade Daemon"
15-
environment: str = "local"
15+
app_environment: str = "local"
16+
environment: str # REQUIRED: must be "prod" or "test" (Hyperliquid API endpoint)
1617
listen_host: str = "0.0.0.0"
1718
listen_port: Optional[int] = None
1819

@@ -30,7 +31,19 @@ class Settings(BaseSettings):
3031
subaccount_addr: Optional[str] = None # allowing None to enable trading on master account
3132

3233
# ── Security & Networking ─────────────────────────────────────────────────
33-
api_url : str = "https://api.hyperliquid.xyz"
34+
@property
35+
def api_url(self) -> str:
36+
"""Derive api_url from HYPERTRADE_ENVIRONMENT."""
37+
if self.environment == "prod":
38+
return "https://api.hyperliquid.xyz"
39+
elif self.environment == "test":
40+
return "https://api.hyperliquid-testnet.xyz"
41+
else:
42+
raise ValueError(
43+
f"Invalid HYPERTRADE_ENVIRONMENT='{self.environment}'. "
44+
"Must be 'prod' or 'test'."
45+
)
46+
3447
ip_whitelist_enabled: bool = False
3548
trust_forwarded_for: bool = True
3649

@@ -77,6 +90,14 @@ class Settings(BaseSettings):
7790
db_enabled: bool = True
7891

7992
# ── Validators ────────────────────────────────────────────────────────────
93+
@field_validator("environment")
94+
@classmethod
95+
def _validate_hyperliquid_environment(cls, value: str) -> str:
96+
value = (value or "").strip().lower()
97+
if value not in {"prod", "test"}:
98+
raise ValueError("must be 'prod' or 'test'")
99+
return value
100+
80101
@field_validator("master_addr")
81102
@classmethod
82103
def _not_blank(cls, value: str) -> str:

hypertrade/daemon.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ def _please_die_gracefully() -> None:
3131
banner = (
3232
"\n"
3333
"╔" + "═" * 72 + "╗\n"
34-
"║ ⚠️ HYPERTRADE DAEMON CANNOT START – MISSING SECRETS ⚠️ \n"
34+
"║ ⚠️ HYPERTRADE DAEMON CANNOT START – MISSING CONFIGURATION ⚠️ ║\n"
3535
"╚" + "═" * 72 + "╝\n"
3636
"\n"
3737
"Required environment variables are not set:\n"
3838
"\n"
39+
" • HYPERTRADE_ENVIRONMENT → 'prod' or 'test' (Hyperliquid endpoint)\n"
3940
" • HYPERTRADE_MASTER_ADDR → your Hyperliquid master address\n"
4041
" • HYPERTRADE_API_WALLET_PRIV → 64-char hex private key (with or without 0x)\n"
4142
" • HYPERTRADE_SUBACCOUNT_ADDR → your sub-account address (optional)\n"
@@ -162,7 +163,8 @@ def _telegram_notify(text: str, _token=token, _chat_id=chat_id):
162163
)
163164
register_exception_handlers(app)
164165
log.info(
165-
"App started env=%s whitelist_enabled=%s log_level=%s",
166+
"App started app_env=%s hl_env=%s whitelist_enabled=%s log_level=%s",
167+
settings.app_environment,
166168
settings.environment,
167169
settings.ip_whitelist_enabled,
168170
settings.log_level,

hypertrade/routes/hyperliquid_data_client.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from __future__ import annotations
44

55
import logging
6-
import os
76
from typing import Any, Dict, Tuple, Optional
87

98
import requests
9+
from hypertrade.config import get_settings
1010

1111
log = logging.getLogger("uvicorn.error")
1212

@@ -18,11 +18,16 @@ class HyperliquidDataClient:
1818
def __init__(
1919
self,
2020
account_address: Optional[str] = None,
21-
base_url: str = "https://api.hyperliquid.xyz",
21+
base_url: Optional[str] = None,
2222
):
2323
"""Create the client with optional account and base URL overrides."""
24+
settings = get_settings()
25+
26+
if base_url is None:
27+
base_url = settings.api_url
28+
2429
self.info_url = base_url.rstrip("/") + "/info"
25-
self.account_address = account_address or os.environ.get("HYPERTRADE_MASTER_ADDR")
30+
self.account_address = account_address or settings.master_addr
2631

2732
log.debug("HyperliquidDataClient initialized | Base URL: %s", self.info_url)
2833

hypertrade/routes/hyperliquid_execution_client.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import os
43
import time
54
import logging
65
from decimal import Decimal, ROUND_FLOOR, ROUND_CEILING
@@ -9,6 +8,7 @@
98

109
from eth_account import Account
1110
from hyperliquid.exchange import Exchange
11+
from hypertrade.config import get_settings
1212
from .hyperliquid_data_client import HyperliquidDataClient
1313

1414
log = logging.getLogger("uvicorn.error")
@@ -46,12 +46,16 @@ def __init__(
4646
private_key: str,
4747
account_address: Optional[str] = None,
4848
vault_address: Optional[str] = None,
49-
base_url: str = "https://api.hyperliquid.xyz",
49+
base_url: Optional[str] = None,
5050
default_premium_bps: float = 5.0,
5151
):
5252
if not private_key:
5353
raise ValueError("private_key must be provided")
54-
54+
55+
if base_url is None:
56+
settings = get_settings()
57+
base_url = settings.api_url
58+
5559
pk = private_key if private_key.startswith("0x") else f"0x{private_key}"
5660
wallet = Account.from_key(pk)
5761

hypertrade/routes/hyperliquid_service.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""Lightweight Hyperliquid client abstraction used by webhook processing."""
22

3-
import os
43
import logging
54
import json
65

76
from dataclasses import dataclass
87
from decimal import Decimal
98
from typing import Optional
109

10+
from hypertrade.config import get_settings
1111
from .tradingview_enums import Side, SignalType
1212
from .hyperliquid_execution_client import HyperliquidExecutionClient, PositionSide
1313

@@ -69,7 +69,11 @@ def __init__(
6969
api_wallet_priv: Optional[str] = None,
7070
subaccount_addr: Optional[str] = None,
7171
):
72-
self.base_url = base_url or os.getenv("HL_BASE_URL", "https://api.hyperliquid.xyz")
72+
if base_url is None:
73+
settings = get_settings()
74+
self.base_url = settings.api_url
75+
else:
76+
self.base_url = base_url
7377
self.subaccount_addr = subaccount_addr
7478

7579
log.debug(

tests/test_health.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
def make_app(monkeypatch):
1313
"""Create app with env configured via pytest monkeypatch."""
1414
# Required env vars for settings
15+
monkeypatch.setenv("HYPERTRADE_ENVIRONMENT", "test")
1516
monkeypatch.setenv("HYPERTRADE_MASTER_ADDR", "0xMASTER")
1617
monkeypatch.setenv("HYPERTRADE_API_WALLET_PRIV", "dummy-priv-key")
1718
monkeypatch.setenv("HYPERTRADE_SUBACCOUNT_ADDR", "0xSUB")

tests/test_subaccount.py

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,45 @@
22

33
from __future__ import annotations
44

5-
from unittest.mock import Mock, patch
5+
from unittest.mock import Mock, patch, MagicMock
66

77
from hypertrade.routes.hyperliquid_service import HyperliquidService
88

99

10+
def _mock_settings():
11+
"""Create a mock settings object."""
12+
mock_settings = MagicMock()
13+
mock_settings.api_url = "https://api.hyperliquid.xyz"
14+
mock_settings.master_addr = "0xDEFAULT"
15+
return mock_settings
16+
17+
1018
def test_hyperliquid_service_stores_subaccount_when_provided():
1119
"""Test that HyperliquidService stores subaccount address."""
1220
subaccount = "0xSUBACCOUNT789"
1321

1422
with patch("hypertrade.routes.hyperliquid_service.HyperliquidExecutionClient"):
15-
service = HyperliquidService(
16-
master_addr="0xMASTER",
17-
api_wallet_priv="test-key",
18-
subaccount_addr=subaccount,
19-
)
23+
with patch("hypertrade.routes.hyperliquid_service.get_settings", return_value=_mock_settings()):
24+
service = HyperliquidService(
25+
master_addr="0xMASTER",
26+
api_wallet_priv="test-key",
27+
subaccount_addr=subaccount,
28+
)
2029

21-
assert service.subaccount_addr == subaccount
30+
assert service.subaccount_addr == subaccount
2231

2332

2433
def test_hyperliquid_service_stores_none_when_no_subaccount():
2534
"""Test that HyperliquidService stores None when subaccount not provided."""
2635
with patch("hypertrade.routes.hyperliquid_service.HyperliquidExecutionClient"):
27-
service = HyperliquidService(
28-
master_addr="0xMASTER",
29-
api_wallet_priv="test-key",
30-
subaccount_addr=None,
31-
)
36+
with patch("hypertrade.routes.hyperliquid_service.get_settings", return_value=_mock_settings()):
37+
service = HyperliquidService(
38+
master_addr="0xMASTER",
39+
api_wallet_priv="test-key",
40+
subaccount_addr=None,
41+
)
3242

33-
assert service.subaccount_addr is None
43+
assert service.subaccount_addr is None
3444

3545

3646
def test_hyperliquid_client_initialized_with_subaccount():
@@ -40,33 +50,35 @@ def test_hyperliquid_client_initialized_with_subaccount():
4050
with patch(
4151
"hypertrade.routes.hyperliquid_service.HyperliquidExecutionClient"
4252
) as mock_client_class:
43-
service = HyperliquidService(
44-
master_addr="0xMASTER",
45-
api_wallet_priv="test-key",
46-
subaccount_addr=subaccount,
47-
)
53+
with patch("hypertrade.routes.hyperliquid_service.get_settings", return_value=_mock_settings()):
54+
service = HyperliquidService(
55+
master_addr="0xMASTER",
56+
api_wallet_priv="test-key",
57+
subaccount_addr=subaccount,
58+
)
4859

49-
# Verify HyperliquidExecutionClient was called with subaccount
50-
mock_client_class.assert_called_once()
51-
call_kwargs = mock_client_class.call_args[1]
52-
assert call_kwargs["vault_address"] == subaccount
60+
# Verify HyperliquidExecutionClient was called with subaccount
61+
mock_client_class.assert_called_once()
62+
call_kwargs = mock_client_class.call_args[1]
63+
assert call_kwargs["vault_address"] == subaccount
5364

5465

5566
def test_hyperliquid_client_initialized_without_subaccount():
5667
"""Test that HyperliquidExecutionClient receives None for vault_address."""
5768
with patch(
5869
"hypertrade.routes.hyperliquid_service.HyperliquidExecutionClient"
5970
) as mock_client_class:
60-
service = HyperliquidService(
61-
master_addr="0xMASTER",
62-
api_wallet_priv="test-key",
63-
subaccount_addr=None,
64-
)
71+
with patch("hypertrade.routes.hyperliquid_service.get_settings", return_value=_mock_settings()):
72+
service = HyperliquidService(
73+
master_addr="0xMASTER",
74+
api_wallet_priv="test-key",
75+
subaccount_addr=None,
76+
)
6577

66-
# Verify HyperliquidExecutionClient was called with None
67-
mock_client_class.assert_called_once()
68-
call_kwargs = mock_client_class.call_args[1]
69-
assert call_kwargs["vault_address"] is None
78+
# Verify HyperliquidExecutionClient was called with None
79+
mock_client_class.assert_called_once()
80+
call_kwargs = mock_client_class.call_args[1]
81+
assert call_kwargs["vault_address"] is None
7082

7183

7284
def test_subaccount_passed_to_execution_client_with_correct_params():
@@ -79,17 +91,18 @@ def test_subaccount_passed_to_execution_client_with_correct_params():
7991
with patch(
8092
"hypertrade.routes.hyperliquid_service.HyperliquidExecutionClient"
8193
) as mock_client_class:
82-
service = HyperliquidService(
83-
base_url=base_url,
84-
master_addr=master,
85-
api_wallet_priv=priv_key,
86-
subaccount_addr=subaccount,
87-
)
88-
89-
# Verify correct parameters were passed
90-
mock_client_class.assert_called_once()
91-
call_kwargs = mock_client_class.call_args[1]
92-
assert call_kwargs["private_key"] == priv_key
93-
assert call_kwargs["account_address"] == master
94-
assert call_kwargs["vault_address"] == subaccount
95-
assert call_kwargs["base_url"] == base_url
94+
with patch("hypertrade.routes.hyperliquid_service.get_settings", return_value=_mock_settings()):
95+
service = HyperliquidService(
96+
base_url=base_url,
97+
master_addr=master,
98+
api_wallet_priv=priv_key,
99+
subaccount_addr=subaccount,
100+
)
101+
102+
# Verify correct parameters were passed
103+
mock_client_class.assert_called_once()
104+
call_kwargs = mock_client_class.call_args[1]
105+
assert call_kwargs["private_key"] == priv_key
106+
assert call_kwargs["account_address"] == master
107+
assert call_kwargs["vault_address"] == subaccount
108+
assert call_kwargs["base_url"] == base_url

tests/test_webhook.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def reset(cls):
9797
def make_app(monkeypatch, *, secret: str | None = None):
9898
"""Create app with env configured via pytest monkeypatch."""
9999
# Required env vars for settings
100+
monkeypatch.setenv("HYPERTRADE_ENVIRONMENT", "test")
100101
monkeypatch.setenv("HYPERTRADE_MASTER_ADDR", "0xMASTER")
101102
monkeypatch.setenv("HYPERTRADE_API_WALLET_PRIV", "dummy-priv-key")
102103
monkeypatch.setenv("HYPERTRADE_SUBACCOUNT_ADDR", "0xSUB")

0 commit comments

Comments
 (0)