Skip to content

Commit d773ffa

Browse files
authored
feat(hip-3-pusher): Configurable price sources (#3169)
1 parent e68ae0a commit d773ffa

File tree

13 files changed

+426
-185
lines changed

13 files changed

+426
-185
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
stale_price_threshold_seconds = 5
2+
prometheus_port = 9090
3+
4+
[hyperliquid]
5+
hyperliquid_ws_urls = ["wss://api.hyperliquid-testnet.xyz/ws"]
6+
market_name = "pyth"
7+
asset_context_symbols = ["BTC"]
8+
use_testnet = false
9+
oracle_pusher_key_path = "/path/to/oracle_pusher_key.txt"
10+
publish_interval = 3.0
11+
publish_timeout = 5.0
12+
enable_publish = false
13+
14+
[multisig]
15+
enable_multisig = false
16+
17+
[kms]
18+
enable_kms = false
19+
aws_kms_key_id_path = "/path/to/aws_kms_key_id.txt"
20+
21+
[lazer]
22+
lazer_urls = ["wss://pyth-lazer-0.dourolabs.app/v1/stream", "wss://pyth-lazer-1.dourolabs.app/v1/stream"]
23+
lazer_api_key = "lazer_api_key"
24+
feed_ids = [1, 8] # BTC, USDT
25+
26+
[hermes]
27+
hermes_urls = ["wss://hermes.pyth.network/ws"]
28+
feed_ids = [
29+
"e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", # BTC
30+
"2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b" # USDT
31+
]
32+
33+
[price.oracle]
34+
BTC = [
35+
{ source_type = "single", source = { source_name = "hl_oracle", source_id = "BTC" } },
36+
{ source_type = "pair", base_source = { source_name = "lazer", source_id = 1, exponent = -8 }, quote_source = { source_name = "lazer", source_id = 8, exponent = -8 } },
37+
{ source_type = "pair", base_source = { source_name = "hermes", source_id = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", exponent = -8 }, quote_source = { source_name = "hermes", source_id = "2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b", exponent = -8 } },
38+
]
39+
40+
[price.external]
41+
BTC = [{ source_type = "single", source = { source_name = "hl_mark", source_id = "BTC" } }]
42+
PYTH = [{ source_type = "constant", value = "0.10" }]
43+
FOGO = [{ source_type = "constant", value = "0.01" }]

apps/hip-3-pusher/config/config.toml

Lines changed: 0 additions & 35 deletions
This file was deleted.

apps/hip-3-pusher/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
[project]
22
name = "hip-3-pusher"
3-
version = "0.1.7"
3+
version = "0.2.0"
44
description = "Hyperliquid HIP-3 market oracle pusher"
55
readme = "README.md"
66
requires-python = "==3.13.*"
77
dependencies = [
88
"boto3~=1.40.38",
99
"cryptography~=46.0.1",
10+
"httpx~=0.28.1",
1011
"hyperliquid-python-sdk~=0.19.0",
1112
"loguru~=0.7.3",
1213
"opentelemetry-exporter-prometheus~=0.58b0",

apps/hip-3-pusher/src/pusher/config.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from hyperliquid.utils.constants import MAINNET_API_URL, TESTNET_API_URL
22
from pydantic import BaseModel, FilePath, model_validator
33
from typing import Optional
4+
from typing import Literal
45

56
STALE_TIMEOUT_SECONDS = 5
67

@@ -18,25 +19,19 @@ class MultisigConfig(BaseModel):
1819
class LazerConfig(BaseModel):
1920
lazer_urls: list[str]
2021
lazer_api_key: str
21-
base_feed_id: int
22-
base_feed_exponent: int
23-
quote_feed_id: int
24-
quote_feed_exponent: int
22+
feed_ids: list[int]
2523

2624

2725
class HermesConfig(BaseModel):
2826
hermes_urls: list[str]
29-
base_feed_id: str
30-
base_feed_exponent: int
31-
quote_feed_id: str
32-
quote_feed_exponent: int
27+
feed_ids: list[str]
3328

3429

3530
class HyperliquidConfig(BaseModel):
3631
hyperliquid_ws_urls: list[str]
3732
push_urls: Optional[list[str]] = None
3833
market_name: str
39-
market_symbol: str
34+
asset_context_symbols: list[str]
4035
use_testnet: bool
4136
oracle_pusher_key_path: Optional[FilePath] = None
4237
publish_interval: float
@@ -50,11 +45,58 @@ def set_default_urls(self):
5045
return self
5146

5247

48+
class SedaFeedConfig(BaseModel):
49+
exec_program_id: str
50+
exec_inputs: str
51+
52+
53+
class SedaConfig(BaseModel):
54+
url: str
55+
api_key_path: Optional[FilePath] = None
56+
poll_interval: float
57+
poll_failure_interval: float
58+
poll_timeout: float
59+
feeds: dict[str, SedaFeedConfig]
60+
61+
62+
class PriceSource(BaseModel):
63+
source_name: str
64+
source_id: str | int
65+
exponent: Optional[int] = None
66+
67+
68+
class SingleSourceConfig(BaseModel):
69+
source_type: Literal["single"]
70+
source: PriceSource
71+
72+
73+
class PairSourceConfig(BaseModel):
74+
source_type: Literal["pair"]
75+
base_source: PriceSource
76+
quote_source: PriceSource
77+
78+
79+
class ConstantSourceConfig(BaseModel):
80+
source_type: Literal["constant"]
81+
value: str
82+
83+
84+
PriceSourceConfig = SingleSourceConfig | PairSourceConfig | ConstantSourceConfig
85+
86+
87+
class PriceConfig(BaseModel):
88+
oracle: dict[str, list[PriceSourceConfig]] = {}
89+
mark: dict[str, list[PriceSourceConfig]] = {}
90+
external: dict[str, list[PriceSourceConfig]] = {}
91+
92+
5393
class Config(BaseModel):
5494
stale_price_threshold_seconds: int
5595
prometheus_port: int
5696
hyperliquid: HyperliquidConfig
5797
kms: KMSConfig
5898
lazer: LazerConfig
5999
hermes: HermesConfig
100+
seda: SedaConfig
60101
multisig: MultisigConfig
102+
price: PriceConfig

apps/hip-3-pusher/src/pusher/hermes_listener.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,22 @@
77

88
from pusher.config import Config, STALE_TIMEOUT_SECONDS
99
from pusher.exception import StaleConnectionError
10-
from pusher.price_state import PriceState, PriceUpdate
10+
from pusher.price_state import PriceSourceState, PriceUpdate
1111

1212

1313
class HermesListener:
1414
"""
1515
Subscribe to Hermes price updates for needed feeds.
1616
"""
17-
def __init__(self, config: Config, price_state: PriceState):
17+
def __init__(self, config: Config, hermes_state: PriceSourceState):
1818
self.hermes_urls = config.hermes.hermes_urls
19-
self.base_feed_id = config.hermes.base_feed_id
20-
self.quote_feed_id = config.hermes.quote_feed_id
21-
self.price_state = price_state
19+
self.feed_ids = config.hermes.feed_ids
20+
self.hermes_state = hermes_state
2221

2322
def get_subscribe_request(self):
2423
return {
2524
"type": "subscribe",
26-
"ids": [self.base_feed_id, self.quote_feed_id],
25+
"ids": self.feed_ids,
2726
"verbose": False,
2827
"binary": True,
2928
"allow_out_of_order": False,
@@ -81,9 +80,6 @@ def parse_hermes_message(self, data):
8180
publish_time = price_object["publish_time"]
8281
logger.debug("Hermes update: {} {} {} {}", id, price, expo, publish_time)
8382
now = time.time()
84-
if id == self.base_feed_id:
85-
self.price_state.hermes_base_price = PriceUpdate(price, now)
86-
if id == self.quote_feed_id:
87-
self.price_state.hermes_quote_price = PriceUpdate(price, now)
83+
self.hermes_state.put(id, PriceUpdate(price, now))
8884
except Exception as e:
8985
logger.error("parse_hermes_message error: {}", e)

apps/hip-3-pusher/src/pusher/hyperliquid_listener.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from pusher.config import Config, STALE_TIMEOUT_SECONDS
99
from pusher.exception import StaleConnectionError
10-
from pusher.price_state import PriceState, PriceUpdate
10+
from pusher.price_state import PriceSourceState, PriceUpdate
1111

1212
# This will be in config, but note here.
1313
# Other RPC providers exist but so far we've seen their support is incomplete.
@@ -20,10 +20,11 @@ class HyperliquidListener:
2020
Subscribe to any relevant Hyperliquid websocket streams
2121
See https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket
2222
"""
23-
def __init__(self, config: Config, price_state: PriceState):
23+
def __init__(self, config: Config, hl_oracle_state: PriceSourceState, hl_mark_state: PriceSourceState):
2424
self.hyperliquid_ws_urls = config.hyperliquid.hyperliquid_ws_urls
25-
self.market_symbol = config.hyperliquid.market_symbol
26-
self.price_state = price_state
25+
self.asset_context_symbols = config.hyperliquid.asset_context_symbols
26+
self.hl_oracle_state = hl_oracle_state
27+
self.hl_mark_state = hl_mark_state
2728

2829
def get_subscribe_request(self, asset):
2930
return {
@@ -44,9 +45,10 @@ async def subscribe_single(self, url):
4445

4546
async def subscribe_single_inner(self, url):
4647
async with websockets.connect(url) as ws:
47-
subscribe_request = self.get_subscribe_request(self.market_symbol)
48-
await ws.send(json.dumps(subscribe_request))
49-
logger.info("Sent subscribe request to {}", url)
48+
for symbol in self.asset_context_symbols:
49+
subscribe_request = self.get_subscribe_request(symbol)
50+
await ws.send(json.dumps(subscribe_request))
51+
logger.info("Sent subscribe request for symbol: {} to {}", symbol, url)
5052

5153
# listen for updates
5254
while True:
@@ -76,10 +78,10 @@ async def subscribe_single_inner(self, url):
7678
def parse_hyperliquid_ws_message(self, message):
7779
try:
7880
ctx = message["data"]["ctx"]
81+
symbol = message["data"]["coin"]
7982
now = time.time()
80-
self.price_state.hl_oracle_price = PriceUpdate(ctx["oraclePx"], now)
81-
self.price_state.hl_mark_price = PriceUpdate(ctx["markPx"], now)
82-
logger.debug("on_activeAssetCtx: oraclePx: {} marketPx: {}", self.price_state.hl_oracle_price,
83-
self.price_state.hl_mark_price)
83+
self.hl_oracle_state.put(symbol, PriceUpdate(ctx["oraclePx"], now))
84+
self.hl_mark_state.put(symbol, PriceUpdate(ctx["markPx"], now))
85+
logger.debug("on_activeAssetCtx: oraclePx: {} marketPx: {}", ctx["oraclePx"], ctx["markPx"])
8486
except Exception as e:
8587
logger.error("parse_hyperliquid_ws_message error: message: {} e: {}", message, e)

apps/hip-3-pusher/src/pusher/lazer_listener.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,24 @@
77

88
from pusher.config import Config, STALE_TIMEOUT_SECONDS
99
from pusher.exception import StaleConnectionError
10-
from pusher.price_state import PriceState, PriceUpdate
10+
from pusher.price_state import PriceSourceState, PriceUpdate
1111

1212

1313
class LazerListener:
1414
"""
1515
Subscribe to Lazer price updates for needed feeds.
1616
"""
17-
def __init__(self, config: Config, price_state: PriceState):
17+
def __init__(self, config: Config, lazer_state: PriceSourceState):
1818
self.lazer_urls = config.lazer.lazer_urls
1919
self.api_key = config.lazer.lazer_api_key
20-
self.base_feed_id = config.lazer.base_feed_id
21-
self.quote_feed_id = config.lazer.quote_feed_id
22-
self.price_state = price_state
20+
self.feed_ids = config.lazer.feed_ids
21+
self.lazer_state = lazer_state
2322

2423
def get_subscribe_request(self, subscription_id: int):
2524
return {
2625
"type": "subscribe",
2726
"subscriptionId": subscription_id,
28-
"priceFeedIds": [self.base_feed_id, self.quote_feed_id],
27+
"priceFeedIds": self.feed_ids,
2928
"properties": ["price"],
3029
"formats": [],
3130
"deliveryFormat": "json",
@@ -54,7 +53,7 @@ async def subscribe_single_inner(self, router_url):
5453
subscribe_request = self.get_subscribe_request(1)
5554

5655
await ws.send(json.dumps(subscribe_request))
57-
logger.info("Sent Lazer subscribe request to {}", router_url)
56+
logger.info("Sent Lazer subscribe request to {} feed_ids {}", router_url, self.feed_ids)
5857

5958
# listen for updates
6059
while True:
@@ -89,9 +88,7 @@ def parse_lazer_message(self, data):
8988
price = feed_update.get("price", None)
9089
if feed_id is None or price is None:
9190
continue
92-
if feed_id == self.base_feed_id:
93-
self.price_state.lazer_base_price = PriceUpdate(price, now)
94-
if feed_id == self.quote_feed_id:
95-
self.price_state.lazer_quote_price = PriceUpdate(price, now)
91+
else:
92+
self.lazer_state.put(feed_id, PriceUpdate(price, now))
9693
except Exception as e:
9794
logger.error("parse_lazer_message error: {}", e)

apps/hip-3-pusher/src/pusher/main.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pusher.hyperliquid_listener import HyperliquidListener
1010
from pusher.lazer_listener import LazerListener
1111
from pusher.hermes_listener import HermesListener
12+
from pusher.seda_listener import SedaListener
1213
from pusher.price_state import PriceState
1314
from pusher.publisher import Publisher
1415
from pusher.metrics import Metrics
@@ -45,15 +46,17 @@ async def main():
4546
metrics = Metrics(config)
4647

4748
publisher = Publisher(config, price_state, metrics)
48-
hyperliquid_listener = HyperliquidListener(config, price_state)
49-
lazer_listener = LazerListener(config, price_state)
50-
hermes_listener = HermesListener(config, price_state)
49+
hyperliquid_listener = HyperliquidListener(config, price_state.hl_oracle_state, price_state.hl_mark_state)
50+
lazer_listener = LazerListener(config, price_state.lazer_state)
51+
hermes_listener = HermesListener(config, price_state.hermes_state)
52+
seda_listener = SedaListener(config, price_state.seda_state)
5153

5254
await asyncio.gather(
5355
publisher.run(),
5456
hyperliquid_listener.subscribe_all(),
5557
lazer_listener.subscribe_all(),
5658
hermes_listener.subscribe_all(),
59+
seda_listener.run(),
5760
)
5861
logger.info("Exiting hip-3-pusher..")
5962

0 commit comments

Comments
 (0)