Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions apps/hip-3-pusher/config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ publish_interval = 3.0
publish_timeout = 5.0
enable_publish = false

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

[kms]
enable_kms = false
aws_kms_key_id_path = "/path/to/aws_kms_key_id.txt"
Expand Down
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.6"
version = "0.1.7"
description = "Hyperliquid HIP-3 market oracle pusher"
readme = "README.md"
requires-python = "==3.13.*"
Expand Down
6 changes: 6 additions & 0 deletions apps/hip-3-pusher/src/pusher/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ class KMSConfig(BaseModel):
aws_kms_key_id_path: Optional[FilePath] = None


class MultisigConfig(BaseModel):
enable_multisig: bool
multisig_address: Optional[str] = None


class LazerConfig(BaseModel):
lazer_urls: list[str]
lazer_api_key: str
Expand Down Expand Up @@ -52,3 +57,4 @@ class Config(BaseModel):
kms: KMSConfig
lazer: LazerConfig
hermes: HermesConfig
multisig: MultisigConfig
68 changes: 62 additions & 6 deletions apps/hip-3-pusher/src/pusher/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from eth_account import Account
from eth_account.signers.local import LocalAccount
from hyperliquid.exchange import Exchange
from hyperliquid.utils.signing import get_timestamp_ms, sign_multi_sig_l1_action_payload

from pusher.config import Config
from pusher.exception import PushError
Expand All @@ -29,19 +30,28 @@ def __init__(self, config: Config, price_state: PriceState, metrics: Metrics):

self.kms_signer = None
self.enable_kms = False
oracle_account = None
self.oracle_account = None
if not config.kms.enable_kms:
oracle_pusher_key = Path(config.hyperliquid.oracle_pusher_key_path).read_text().strip()
oracle_account: LocalAccount = Account.from_key(oracle_pusher_key)
logger.info("oracle pusher local pubkey: {}", oracle_account.address)
self.publisher_exchanges = [Exchange(wallet=oracle_account,
self.oracle_account: LocalAccount = Account.from_key(oracle_pusher_key)
logger.info("oracle pusher local pubkey: {}", self.oracle_account.address)
self.publisher_exchanges = [Exchange(wallet=self.oracle_account,
base_url=url,
timeout=config.hyperliquid.publish_timeout)
for url in self.push_urls]
if config.kms.enable_kms:
# TODO: Add KMS/multisig support
if config.multisig.enable_multisig:
raise Exception("KMS/multisig not yet supported")

self.enable_kms = True
self.kms_signer = KMSSigner(config, self.publisher_exchanges)

if config.multisig.enable_multisig:
if not config.multisig.multisig_address:
raise Exception("Multisig enabled but missing multisig address")
Comment on lines +50 to +52
Copy link
Contributor

@tejasbadadare tejasbadadare Oct 23, 2025

Choose a reason for hiding this comment

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

You can enforce this directly in the pydantic model. Doing as much validation as possible in the model itself aligns with pydantic's philosophy of "if i can construct the object, i know it's valid."

class MultisigConfig(BaseModel):
    enable_multisig: bool
    multisig_address: Optional[str] = None

    @model_validator(mode="after")
    def validate_address_exists_if_multisig_enabled(self) -> 'MultisigConfig':
        if self.enable_multisig and self.multisig_address is None:
            raise ValueError("`multisig_address` must be provided when `enable_multisig` is True")
        return self

self.multisig_address = config.multisig.multisig_address

self.market_name = config.hyperliquid.market_name
self.market_symbol = config.hyperliquid.market_symbol
self.enable_publish = config.hyperliquid.enable_publish
Expand All @@ -68,12 +78,12 @@ def publish(self):
return
else:
logger.debug("Current oracle price: {}", oracle_px)
oracle_pxs[self.market_symbol] = oracle_px
oracle_pxs[f"{self.market_name}:{self.market_symbol}"] = oracle_px

mark_pxs = []
external_perp_pxs = {}
if self.price_state.hl_mark_price:
external_perp_pxs[self.market_symbol] = self.price_state.hl_mark_price.price
external_perp_pxs[f"{self.market_name}:{self.market_symbol}"] = self.price_state.hl_mark_price.price

# TODO: "Each update can change oraclePx and markPx by at most 1%."
# TODO: "The markPx cannot be updated such that open interest would be 10x the open interest cap."
Expand All @@ -87,6 +97,12 @@ def publish(self):
all_mark_pxs=mark_pxs,
external_perp_pxs=external_perp_pxs,
)
elif self.multisig_address:
push_response = self._send_multisig_update(
oracle_pxs=oracle_pxs,
all_mark_pxs=mark_pxs,
external_perp_pxs=external_perp_pxs,
)
else:
push_response = self._send_update(
oracle_pxs=oracle_pxs,
Expand Down Expand Up @@ -133,3 +149,43 @@ def _record_push_interval_metric(self):
self.metrics.push_interval_histogram.record(push_interval, self.metrics_labels)
self.last_push_time = now
logger.debug("Push interval: {}", push_interval)

def _send_multisig_update(self, oracle_pxs, all_mark_pxs, external_perp_pxs):
for exchange in self.publisher_exchanges:
try:
return self._send_single_multisig_update(
exchange=exchange,
oracle_pxs=oracle_pxs,
all_mark_pxs=all_mark_pxs,
external_perp_pxs=external_perp_pxs,
)
except Exception as e:
logger.exception("_send_single_multisig_update exception for endpoint: {} error: {}", exchange.base_url, repr(e))

raise PushError("all push endpoints failed for multisig")

def _send_single_multisig_update(self, exchange, oracle_pxs, all_mark_pxs, external_perp_pxs):
timestamp = get_timestamp_ms()
oracle_pxs_wire = sorted(list(oracle_pxs.items()))
mark_pxs_wire = [sorted(list(mark_pxs.items())) for mark_pxs in all_mark_pxs]
external_perp_pxs_wire = sorted(list(external_perp_pxs.items()))
action = {
"type": "perpDeploy",
"setOracle": {
"dex": self.market_name,
"oraclePxs": oracle_pxs_wire,
"markPxs": mark_pxs_wire,
"externalPerpPxs": external_perp_pxs_wire,
},
}
signatures = [sign_multi_sig_l1_action_payload(
wallet=self.oracle_account,
action=action,
is_mainnet=not self.use_testnet,
vault_address=None,
timestamp=timestamp,
expires_after=None,
payload_multi_sig_user=self.multisig_address,
outer_signer=self.oracle_account.address,
)]
return exchange.multi_sig(self.multisig_address, action, signatures, timestamp)
2 changes: 1 addition & 1 deletion apps/hip-3-pusher/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.