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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ ignore_missing_imports = true

[tool.poetry]
name = "pyth-observer"
version = "0.2.7"
version = "0.2.8"
description = "Alerts and stuff"
authors = []
readme = "README.md"
Expand Down
1 change: 1 addition & 0 deletions pyth_observer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ async def run(self):
PublisherState(
publisher_name=publisher_name,
symbol=product.attrs["symbol"],
asset_type=product.attrs["asset_type"],
public_key=component.publisher_key,
confidence_interval=component.latest_price_info.confidence_interval,
confidence_interval_aggregate=price_account.aggregate_price_info.confidence_interval,
Expand Down
27 changes: 27 additions & 0 deletions pyth_observer/check/publisher.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import time
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, Protocol, runtime_checkable
from zoneinfo import ZoneInfo

from pythclient.calendar import is_market_open
from pythclient.pythaccounts import PythPriceStatus
from pythclient.solana import SolanaPublicKey

Expand All @@ -14,6 +17,7 @@
class PublisherState:
publisher_name: str
symbol: str
asset_type: str
public_key: SolanaPublicKey
status: PythPriceStatus
aggregate_status: PythPriceStatus
Expand Down Expand Up @@ -139,6 +143,14 @@ def state(self) -> PublisherState:
return self.__state

def run(self) -> bool:
market_open = is_market_open(
self.__state.asset_type.lower(),
datetime.now(ZoneInfo("America/New_York")),
)

if not market_open:
return True

distance = self.__state.latest_block_slot - self.__state.slot

# Pass if publisher slot is not too far from aggregate slot
Expand Down Expand Up @@ -225,11 +237,26 @@ def __init__(self, state: PublisherState, config: PublisherCheckConfig):
self.__stall_time_limit: int = int(
config["stall_time_limit"]
) # Time in seconds
self.__max_slot_distance: int = int(config["max_slot_distance"])

def state(self) -> PublisherState:
return self.__state

def run(self) -> bool:
market_open = is_market_open(
self.__state.asset_type.lower(),
datetime.now(ZoneInfo("America/New_York")),
)

if not market_open:
return True

distance = self.__state.latest_block_slot - self.__state.slot

# Pass when publisher is offline because PublisherOfflineCheck will be triggered
if distance >= self.__max_slot_distance:
return True

publisher_key = (self.__state.publisher_name, self.__state.symbol)
current_time = time.time()
previous_price, last_change_time = PUBLISHER_CACHE.get(
Expand Down
25 changes: 20 additions & 5 deletions tests/test_checks_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def make_state(
return PublisherState(
publisher_name="publisher",
symbol="Crypto.BTC/USD",
asset_type="Crypto",
public_key=SolanaPublicKey("2hgu6Umyokvo8FfSDdMa9nDKhcdv9Q4VvGNhRCeSWeD3"),
status=PythPriceStatus.TRADING,
aggregate_status=PythPriceStatus.TRADING,
Expand Down Expand Up @@ -62,8 +63,14 @@ def simulate_time_pass(seconds):
current_time += seconds
return current_time

def setup_check(state, stall_time_limit):
check = PublisherStalledCheck(state, {"stall_time_limit": stall_time_limit})
def setup_check(state, stall_time_limit, max_slot_distance):
check = PublisherStalledCheck(
state,
{
"stall_time_limit": stall_time_limit,
"max_slot_distance": max_slot_distance,
},
)
PUBLISHER_CACHE[(state.publisher_name, state.symbol)] = (
state.price,
current_time,
Expand All @@ -76,17 +83,17 @@ def run_check(check, seconds, expected):

PUBLISHER_CACHE.clear()
state_a = make_state(1, 100.0, 2.0, 1, 100.0, 1.0)
check_a = setup_check(state_a, 5)
check_a = setup_check(state_a, 5, 25)
run_check(check_a, 5, True) # Should pass as it hits the limit exactly

PUBLISHER_CACHE.clear()
state_b = make_state(1, 100.0, 2.0, 1, 100.0, 1.0)
check_b = setup_check(state_b, 5)
check_b = setup_check(state_b, 5, 25)
run_check(check_b, 6, False) # Should fail as it exceeds the limit

PUBLISHER_CACHE.clear()
state_c = make_state(1, 100.0, 2.0, 1, 100.0, 1.0)
check_c = setup_check(state_c, 5)
check_c = setup_check(state_c, 5, 25)
run_check(check_c, 2, True) # Initial check should pass
state_c.price = 105.0 # Change the price
run_check(check_c, 3, True) # Should pass as price changes
Expand All @@ -95,3 +102,11 @@ def run_check(check, seconds, expected):
run_check(
check_c, 8, False
) # Should fail as price stalls for too long after last change

# Adding a check for when the publisher is offline
PUBLISHER_CACHE.clear()
state_d = make_state(1, 100.0, 2.0, 1, 100.0, 1.0)
state_d.latest_block_slot = 25
state_d.slot = 0
check_d = setup_check(state_d, 5, 25)
run_check(check_d, 10, True) # Should pass as the publisher is offline