Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions apps/hip-3-pusher/config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ prometheus_port = 9090

[hyperliquid]
hyperliquid_ws_urls = ["wss://api.hyperliquid-testnet.xyz/ws"]
market_name = ""
market_symbol = "BTC"
market_name = "pyth"
asset_context_symbols = ["BTC"]
use_testnet = false
oracle_pusher_key_path = "/path/to/oracle_pusher_key.txt"
publish_interval = 3.0
Expand All @@ -13,7 +13,6 @@ enable_publish = false

[multisig]
enable_multisig = false
multisig_address = "0x0000000000000000000000000000000000000005"

[kms]
enable_kms = false
Expand All @@ -22,14 +21,23 @@ aws_kms_key_id_path = "/path/to/aws_kms_key_id.txt"
[lazer]
lazer_urls = ["wss://pyth-lazer-0.dourolabs.app/v1/stream", "wss://pyth-lazer-1.dourolabs.app/v1/stream"]
lazer_api_key = "lazer_api_key"
base_feed_id = 1 # BTC
base_feed_exponent = -8
quote_feed_id = 8 # USDT
quote_feed_exponent = -8
feed_ids = [1, 8] # BTC, USDT

[hermes]
hermes_urls = ["wss://hermes.pyth.network/ws"]
base_feed_id = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" # BTC
base_feed_exponent = -8
quote_feed_id = "2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b" # USDT
quote_feed_exponent = -8
feed_ids = [
"e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", # BTC
"2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b" # USDT
]

[price.oracle]
BTC = [
{ source_type = "single", source = { source_name = "hl_oracle", source_id = "BTC" } },
{ source_type = "pair", base_source = { source_name = "lazer", source_id = 1, exponent = -8 }, quote_source = { source_name = "lazer", source_id = 8, exponent = -8 } },
{ source_type = "pair", base_source = { source_name = "hermes", source_id = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", exponent = -8 }, quote_source = { source_name = "hermes", source_id = "2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b", exponent = -8 } },
]

[price.external]
BTC = [{ source_type = "single", source = { source_name = "hl_mark", source_id = "BTC" } }]
PYTH = [{ source_type = "constant", value = "0.10" }]
FOGO = [{ source_type = "constant", value = "0.01" }]
2 changes: 1 addition & 1 deletion apps/hip-3-pusher/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "hip-3-pusher"
version = "0.1.7"
version = "0.2.0"
description = "Hyperliquid HIP-3 market oracle pusher"
readme = "README.md"
requires-python = "==3.13.*"
Expand Down
45 changes: 36 additions & 9 deletions apps/hip-3-pusher/src/pusher/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from hyperliquid.utils.constants import MAINNET_API_URL, TESTNET_API_URL
from pydantic import BaseModel, FilePath, model_validator
from typing import Optional
from typing import Literal, Union

STALE_TIMEOUT_SECONDS = 5

Expand All @@ -18,25 +19,19 @@ class MultisigConfig(BaseModel):
class LazerConfig(BaseModel):
lazer_urls: list[str]
lazer_api_key: str
base_feed_id: int
base_feed_exponent: int
quote_feed_id: int
quote_feed_exponent: int
feed_ids: list[int]


class HermesConfig(BaseModel):
hermes_urls: list[str]
base_feed_id: str
base_feed_exponent: int
quote_feed_id: str
quote_feed_exponent: int
feed_ids: list[str]


class HyperliquidConfig(BaseModel):
hyperliquid_ws_urls: list[str]
push_urls: Optional[list[str]] = None
market_name: str
market_symbol: str
asset_context_symbols: list[str]
use_testnet: bool
oracle_pusher_key_path: Optional[FilePath] = None
publish_interval: float
Expand All @@ -50,6 +45,37 @@ def set_default_urls(self):
return self


class PriceSource(BaseModel):
source_name: str
source_id: str | int
exponent: Optional[int] = None


class SingleSourceConfig(BaseModel):
source_type: Literal["single"]
source: PriceSource


class PairSourceConfig(BaseModel):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess generally we would leave "combining feeds" up to SEDA? Or would we use this PairSourceConfig for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set this up specifcally for the case (as in our internal publishers) where we need to quote in e.g. USDT. For more complicated transformations yes, hopefully SEDA can abstract it out.

source_type: Literal["pair"]
base_source: PriceSource
quote_source: PriceSource


class ConstantSourceConfig(BaseModel):
source_type: Literal["constant"]
value: str


PriceSourceConfig = Union[SingleSourceConfig, PairSourceConfig, ConstantSourceConfig]


class PriceConfig(BaseModel):
oracle: dict[str, list[PriceSourceConfig]] = {}
mark: dict[str, list[PriceSourceConfig]] = {}
external: dict[str, list[PriceSourceConfig]] = {}


class Config(BaseModel):
stale_price_threshold_seconds: int
prometheus_port: int
Expand All @@ -58,3 +84,4 @@ class Config(BaseModel):
lazer: LazerConfig
hermes: HermesConfig
multisig: MultisigConfig
price: PriceConfig
12 changes: 5 additions & 7 deletions apps/hip-3-pusher/src/pusher/hermes_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@


class HermesListener:
SOURCE_NAME = "hermes"

"""
Subscribe to Hermes price updates for needed feeds.
"""
def __init__(self, config: Config, price_state: PriceState):
self.hermes_urls = config.hermes.hermes_urls
self.base_feed_id = config.hermes.base_feed_id
self.quote_feed_id = config.hermes.quote_feed_id
self.feed_ids = config.hermes.feed_ids
self.price_state = price_state

def get_subscribe_request(self):
return {
"type": "subscribe",
"ids": [self.base_feed_id, self.quote_feed_id],
"ids": self.feed_ids,
"verbose": False,
"binary": True,
"allow_out_of_order": False,
Expand Down Expand Up @@ -81,9 +82,6 @@ def parse_hermes_message(self, data):
publish_time = price_object["publish_time"]
logger.debug("Hermes update: {} {} {} {}", id, price, expo, publish_time)
now = time.time()
if id == self.base_feed_id:
self.price_state.hermes_base_price = PriceUpdate(price, now)
if id == self.quote_feed_id:
self.price_state.hermes_quote_price = PriceUpdate(price, now)
self.price_state.state[self.SOURCE_NAME][id] = PriceUpdate(price, now)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a bit strange to me that we are using a mono-state, but indexing into it with "source name" to distinguish between states of different modules. It would be safer and more explicit to have distinct state objects (which you can still nest within the mono-state, like how Hermes does it,) something like HermesState / LazerState / HLState. The state objects can be passed to the Listener objects during instantiation, which guarantees a listener is scoped to its specific state, and can only mutate the data within.

In the price consumers, you can also more safely reference price_state.hermes_state instead of price_state[self.SOURCE_NAME]. The state objects could also live inside of the Listener classes themselves to reduce indirection if desired.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it, will change.

except Exception as e:
logger.error("parse_hermes_message error: {}", e)
20 changes: 12 additions & 8 deletions apps/hip-3-pusher/src/pusher/hyperliquid_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@


class HyperliquidListener:
ORACLE_SOURCE_NAME = "hl_oracle"
MARK_SOURCE_NAME = "hl_mark"

"""
Subscribe to any relevant Hyperliquid websocket streams
See https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket
"""
def __init__(self, config: Config, price_state: PriceState):
self.hyperliquid_ws_urls = config.hyperliquid.hyperliquid_ws_urls
self.market_symbol = config.hyperliquid.market_symbol
self.asset_context_symbols = config.hyperliquid.asset_context_symbols
self.price_state = price_state

def get_subscribe_request(self, asset):
Expand All @@ -44,9 +47,10 @@ async def subscribe_single(self, url):

async def subscribe_single_inner(self, url):
async with websockets.connect(url) as ws:
subscribe_request = self.get_subscribe_request(self.market_symbol)
await ws.send(json.dumps(subscribe_request))
logger.info("Sent subscribe request to {}", url)
for symbol in self.asset_context_symbols:
subscribe_request = self.get_subscribe_request(symbol)
await ws.send(json.dumps(subscribe_request))
logger.info("Sent subscribe request for symbol: {} to {}", symbol, url)

# listen for updates
while True:
Expand Down Expand Up @@ -76,10 +80,10 @@ async def subscribe_single_inner(self, url):
def parse_hyperliquid_ws_message(self, message):
try:
ctx = message["data"]["ctx"]
symbol = message["data"]["coin"]
now = time.time()
self.price_state.hl_oracle_price = PriceUpdate(ctx["oraclePx"], now)
self.price_state.hl_mark_price = PriceUpdate(ctx["markPx"], now)
logger.debug("on_activeAssetCtx: oraclePx: {} marketPx: {}", self.price_state.hl_oracle_price,
self.price_state.hl_mark_price)
self.price_state.state[self.ORACLE_SOURCE_NAME][symbol] = PriceUpdate(ctx["oraclePx"], now)
self.price_state.state[self.MARK_SOURCE_NAME][symbol] = PriceUpdate(ctx["markPx"], now)
logger.debug("on_activeAssetCtx: oraclePx: {} marketPx: {}", ctx["oraclePx"], ctx["markPx"])
except Exception as e:
logger.error("parse_hyperliquid_ws_message error: message: {} e: {}", message, e)
15 changes: 7 additions & 8 deletions apps/hip-3-pusher/src/pusher/lazer_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@


class LazerListener:
SOURCE_NAME = "lazer"

"""
Subscribe to Lazer price updates for needed feeds.
"""
def __init__(self, config: Config, price_state: PriceState):
self.lazer_urls = config.lazer.lazer_urls
self.api_key = config.lazer.lazer_api_key
self.base_feed_id = config.lazer.base_feed_id
self.quote_feed_id = config.lazer.quote_feed_id
self.feed_ids = config.lazer.feed_ids
self.price_state = price_state

def get_subscribe_request(self, subscription_id: int):
return {
"type": "subscribe",
"subscriptionId": subscription_id,
"priceFeedIds": [self.base_feed_id, self.quote_feed_id],
"priceFeedIds": self.feed_ids,
"properties": ["price"],
"formats": [],
"deliveryFormat": "json",
Expand Down Expand Up @@ -54,7 +55,7 @@ async def subscribe_single_inner(self, router_url):
subscribe_request = self.get_subscribe_request(1)

await ws.send(json.dumps(subscribe_request))
logger.info("Sent Lazer subscribe request to {}", router_url)
logger.info("Sent Lazer subscribe request to {} feed_ids {}", router_url, self.feed_ids)

# listen for updates
while True:
Expand Down Expand Up @@ -89,9 +90,7 @@ def parse_lazer_message(self, data):
price = feed_update.get("price", None)
if feed_id is None or price is None:
continue
if feed_id == self.base_feed_id:
self.price_state.lazer_base_price = PriceUpdate(price, now)
if feed_id == self.quote_feed_id:
self.price_state.lazer_quote_price = PriceUpdate(price, now)
else:
self.price_state.state[self.SOURCE_NAME][feed_id] = PriceUpdate(price, now)
except Exception as e:
logger.error("parse_lazer_message error: {}", e)
Loading