From ffdd9340243f9223ecc0764be9cf976476044cba Mon Sep 17 00:00:00 2001 From: ani Date: Mon, 19 Feb 2024 22:06:31 -0500 Subject: [PATCH 01/10] hermes support --- examples/read_hermes.py | 35 ++++++++ pythclient/hermes.py | 173 ++++++++++++++++++++++++++++++++++++++ pythclient/price_feeds.py | 17 +++- tests/test_hermes.py | 15 ++++ 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 examples/read_hermes.py create mode 100644 pythclient/hermes.py create mode 100644 tests/test_hermes.py diff --git a/examples/read_hermes.py b/examples/read_hermes.py new file mode 100644 index 0000000..a8aea0e --- /dev/null +++ b/examples/read_hermes.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +import asyncio + +from pythclient.hermes import HermesClient, PriceFeed + +async def get_hermes_prices(): + hermes_client = HermesClient([]) + feed_ids = await hermes_client.get_price_feed_ids() + feed_ids_rel = feed_ids[:2] + + hermes_client.add_feed_ids(feed_ids_rel) + + prices_latest = await hermes_client.get_all_prices() + + sd = list(prices_latest.keys())[0] + import pdb; pdb.set_trace() + + for feed_id, price_feed in prices_latest.items(): + print("Initial prices") + price_latest = price_feed["price"].price + conf_latest = price_feed["price"].conf + print(f"Feed ID: {feed_id}, Price: {price_latest}, Confidence: {conf_latest}, Time: {price_feed['price'].publish_time}") + + print("Starting web socket...") + ws_call = hermes_client.ws_pyth_prices() + asyncio.create_task(ws_call) + + while True: + await asyncio.sleep(5) + print("Latest prices:") + for feed_id, price_feed in hermes_client.prices_dict.items(): + print(f"Feed ID: {feed_id}, Price: {price_latest}, Confidence: {conf_latest}, Time: {price_feed['price'].publish_time}") + +asyncio.run(get_hermes_prices()) diff --git a/pythclient/hermes.py b/pythclient/hermes.py new file mode 100644 index 0000000..d08b9fa --- /dev/null +++ b/pythclient/hermes.py @@ -0,0 +1,173 @@ +import asyncio +from typing import TypedDict + +import httpx +import os + +from .price_feeds import Price + +HERMES_ENDPOINT_HTTPS = "https://hermes.pyth.network/api/" +HERMES_ENDPOINT_WSS = "wss://hermes.pyth.network/ws" + + +class PriceFeed(TypedDict): + feed_id: str + price: Price + ema_price: Price + vaa: str + + + +class HermesClient: + def __init__(self, feed_ids: list[str], endpoint=HERMES_ENDPOINT_HTTPS, ws_endpoint=HERMES_ENDPOINT_WSS): + self.feed_ids = feed_ids + self.pending_feed_ids = feed_ids + self.prices_dict: dict[str, PriceFeed] = {} + self.client = httpx.AsyncClient() + self.endpoint = endpoint + self.ws_endpoint = ws_endpoint + + async def get_price_feed_ids(self) -> list[str]: + """ + Queries the Hermes https endpoint for a list of the IDs of all Pyth price feeds. + """ + + url = os.path.join(self.endpoint, "price_feed_ids") + + client = httpx.AsyncClient() + + data = (await client.get(url)).json() + + return data + + def add_feed_ids(self, feed_ids: list[str]): + self.feed_ids += feed_ids + self.feed_ids = list(set(self.feed_ids)) + self.pending_feed_ids += feed_ids + + @staticmethod + def extract_price_feed(data: dict) -> PriceFeed: + """ + Extracts a PriceFeed object from the JSON response from Hermes. + """ + price = Price.from_dict(data["price"]) + ema_price = Price.from_dict(data["ema_price"]) + vaa = data["vaa"] + price_feed = { + "feed_id": data["id"], + "price": price, + "ema_price": ema_price, + "vaa": vaa, + } + return price_feed + + async def get_pyth_prices_latest(self, feedIds: list[str]) -> list[PriceFeed]: + """ + Queries the Hermes https endpoint for the latest price feeds for a list of Pyth feed IDs. + """ + url = os.path.join(self.endpoint, "latest_price_feeds?") + params = {"ids[]": feedIds, "binary": "true"} + + data = (await self.client.get(url, params=params)).json() + + results = [] + for res in data: + price_feed = self.extract_price_feed(res) + results.append(price_feed) + + return results + + async def get_pyth_price_at_time(self, feed_id: str, timestamp: int) -> PriceFeed: + """ + Queries the Hermes https endpoint for the price feed for a Pyth feed ID at a given timestamp. + """ + url = os.path.join(self.endpoint, "get_price_feed") + params = {"id": feed_id, "publish_time": timestamp, "binary": "true"} + + data = (await self.client.get(url, params=params)).json() + + price_feed = self.extract_price_feed(data) + + return price_feed + + async def get_all_prices(self) -> dict[str, PriceFeed]: + """ + Queries the Hermes http endpoint for the latest price feeds for all feed IDs in the class object. + + There are limitations on the number of feed IDs that can be queried at once, so this function queries the feed IDs in batches. + """ + pyth_prices_latest = [] + i = 0 + batch_size = 100 + while len(self.feed_ids[i : i + batch_size]) > 0: + pyth_prices_latest += await self.get_pyth_prices_latest( + self.feed_ids[i : i + batch_size] + ) + i += batch_size + + return dict([(feed['feed_id'], feed) for feed in pyth_prices_latest]) + + async def ws_pyth_prices(self): + """ + Opens a websocket connection to Hermes for latest prices for all feed IDs in the class object. + """ + import json + + import websockets + + async with websockets.connect(self.ws_endpoint) as ws: + while True: + # add new price feed ids to the ws subscription + if len(self.pending_feed_ids) > 0: + json_subscribe = { + "ids": self.pending_feed_ids, + "type": "subscribe", + "verbose": True, + "binary": True, + } + await ws.send(json.dumps(json_subscribe)) + self.pending_feed_ids = [] + + msg = json.loads(await ws.recv()) + if msg.get("type") == "response": + if msg.get("status") != "success": + raise Exception("Error in subscribing to websocket") + try: + if msg["type"] != "price_update": + continue + + feed_id = msg["price_feed"]["id"] + new_feed = msg["price_feed"] + + self.prices_dict[feed_id] = self.extract_price_feed(new_feed) + + except: + raise Exception("Error in price_update message", msg) + + +async def main(): + hermes_client = HermesClient([]) + feed_ids = await hermes_client.get_price_feed_ids() + feed_ids_rel = feed_ids[:50] + + hermes_client.add_feed_ids(feed_ids_rel) + + prices_latest = await hermes_client.get_pyth_prices_latest(feed_ids_rel) + + try: + price_at_time = await hermes_client.get_pyth_price_at_time(feed_ids[0], 1_700_000_000) + except Exception as e: + print(f"Error in get_pyth_price_at_time, {e}") + + all_prices = await hermes_client.get_all_prices() + + print("Starting web socket...") + ws_call = hermes_client.ws_pyth_prices() + asyncio.create_task(ws_call) + + while True: + await asyncio.sleep(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/pythclient/price_feeds.py b/pythclient/price_feeds.py index d5275f6..4c1d031 100644 --- a/pythclient/price_feeds.py +++ b/pythclient/price_feeds.py @@ -1,7 +1,7 @@ import base64 import binascii from struct import unpack -from typing import List, Literal, Optional, Union, cast +from typing import List, Literal, Optional, Union, cast, TypedDict from Crypto.Hash import keccak from loguru import logger @@ -17,6 +17,11 @@ MAX_MESSAGE_IN_SINGLE_UPDATE_DATA = 255 +class PriceDict(TypedDict): + conf: str + expo: int + price: str + publish_time: int class Price: def __init__(self, conf, expo, price, publish_time) -> None: @@ -35,6 +40,16 @@ def to_dict(self): "price": self.price, "publish_time": self.publish_time, } + + @staticmethod + def from_dict(price_dict: PriceDict): + return Price( + conf=int(price_dict["conf"]), + expo=price_dict["expo"], + price=int(price_dict["price"]), + publish_time=price_dict["publish_time"], + ) + class PriceUpdate: diff --git a/tests/test_hermes.py b/tests/test_hermes.py new file mode 100644 index 0000000..45b8ab3 --- /dev/null +++ b/tests/test_hermes.py @@ -0,0 +1,15 @@ +from pythclient.hermes import HermesClient, PriceFeed + +BTC_ID = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" # BTC/USD +ETH_ID = "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace" # ETH/USD + +async def test_hermes_return_price_feed_object(): + # Test that the hermes get request returns a dict with same keys as PriceFeed + hermes_client = HermesClient([]) + hermes_client.add_feed_ids([BTC_ID, ETH_ID]) + + all_prices = await hermes_client.get_all_prices() + + assert isinstance(all_prices, dict) + assert set(all_prices[BTC_ID].keys()) == set(PriceFeed.__annotations__.keys()) + assert set(all_prices[ETH_ID].keys()) == set(PriceFeed.__annotations__.keys()) \ No newline at end of file From 5691b9ff1864541df82a93ef4f21ddcac71b99db Mon Sep 17 00:00:00 2001 From: ani Date: Mon, 19 Feb 2024 22:16:25 -0500 Subject: [PATCH 02/10] add dep --- examples/read_hermes.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/read_hermes.py b/examples/read_hermes.py index a8aea0e..b5ac53e 100644 --- a/examples/read_hermes.py +++ b/examples/read_hermes.py @@ -2,7 +2,7 @@ import asyncio -from pythclient.hermes import HermesClient, PriceFeed +from pythclient.hermes import HermesClient async def get_hermes_prices(): hermes_client = HermesClient([]) diff --git a/setup.py b/setup.py index 6a19d5d..4ec4447 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -requirements = ['aiodns', 'aiohttp>=3.7.4', 'backoff', 'base58', 'flake8', 'loguru', 'typing-extensions', 'pytz', 'pycryptodome'] +requirements = ['aiodns', 'aiohttp>=3.7.4', 'backoff', 'base58', 'flake8', 'loguru', 'typing-extensions', 'pytz', 'pycryptodome', 'httpx'] with open('README.md', 'r', encoding='utf-8') as fh: long_description = fh.read() From 83897373b68c65c31bb5e91b3659db4d1cd95e2e Mon Sep 17 00:00:00 2001 From: ani Date: Tue, 20 Feb 2024 12:54:34 -0500 Subject: [PATCH 03/10] address comments --- examples/read_hermes.py | 6 +- pythclient/hermes.py | 127 +++++++++++++++++++++++++++------------- tests/test_hermes.py | 112 +++++++++++++++++++++++++++++++---- 3 files changed, 192 insertions(+), 53 deletions(-) diff --git a/examples/read_hermes.py b/examples/read_hermes.py index b5ac53e..325c2df 100644 --- a/examples/read_hermes.py +++ b/examples/read_hermes.py @@ -8,13 +8,13 @@ async def get_hermes_prices(): hermes_client = HermesClient([]) feed_ids = await hermes_client.get_price_feed_ids() feed_ids_rel = feed_ids[:2] + version = 1 hermes_client.add_feed_ids(feed_ids_rel) - prices_latest = await hermes_client.get_all_prices() + prices_latest = await hermes_client.get_all_prices(version=version) sd = list(prices_latest.keys())[0] - import pdb; pdb.set_trace() for feed_id, price_feed in prices_latest.items(): print("Initial prices") @@ -23,7 +23,7 @@ async def get_hermes_prices(): print(f"Feed ID: {feed_id}, Price: {price_latest}, Confidence: {conf_latest}, Time: {price_feed['price'].publish_time}") print("Starting web socket...") - ws_call = hermes_client.ws_pyth_prices() + ws_call = hermes_client.ws_pyth_prices(version=version) asyncio.create_task(ws_call) while True: diff --git a/pythclient/hermes.py b/pythclient/hermes.py index d08b9fa..657d3e7 100644 --- a/pythclient/hermes.py +++ b/pythclient/hermes.py @@ -1,12 +1,13 @@ import asyncio from typing import TypedDict - import httpx import os +import json +import websockets from .price_feeds import Price -HERMES_ENDPOINT_HTTPS = "https://hermes.pyth.network/api/" +HERMES_ENDPOINT_HTTPS = "https://hermes.pyth.network/" HERMES_ENDPOINT_WSS = "wss://hermes.pyth.network/ws" @@ -14,29 +15,34 @@ class PriceFeed(TypedDict): feed_id: str price: Price ema_price: Price - vaa: str + update_data: list[str] + +def parse_unsupported_version(version): + if isinstance(version, int): + raise ValueError("Version number {version} not supported") + else: + raise TypeError("Version must be an integer") class HermesClient: - def __init__(self, feed_ids: list[str], endpoint=HERMES_ENDPOINT_HTTPS, ws_endpoint=HERMES_ENDPOINT_WSS): + def __init__(self, feed_ids: list[str], endpoint=HERMES_ENDPOINT_HTTPS, ws_endpoint=HERMES_ENDPOINT_WSS, feed_batch_size=100): self.feed_ids = feed_ids self.pending_feed_ids = feed_ids self.prices_dict: dict[str, PriceFeed] = {} self.client = httpx.AsyncClient() self.endpoint = endpoint self.ws_endpoint = ws_endpoint + self.feed_batch_size = feed_batch_size async def get_price_feed_ids(self) -> list[str]: """ Queries the Hermes https endpoint for a list of the IDs of all Pyth price feeds. """ - url = os.path.join(self.endpoint, "price_feed_ids") - - client = httpx.AsyncClient() + url = os.path.join(self.endpoint, "api/price_feed_ids") - data = (await client.get(url)).json() + data = (await self.client.get(url)).json() return data @@ -46,51 +52,91 @@ def add_feed_ids(self, feed_ids: list[str]): self.pending_feed_ids += feed_ids @staticmethod - def extract_price_feed(data: dict) -> PriceFeed: + def extract_price_feed_v1(data: dict) -> PriceFeed: """ - Extracts a PriceFeed object from the JSON response from Hermes. + Extracts PriceFeed object from the v1 JSON response (individual price feed) from Hermes. """ price = Price.from_dict(data["price"]) ema_price = Price.from_dict(data["ema_price"]) - vaa = data["vaa"] + update_data = data["vaa"] price_feed = { "feed_id": data["id"], "price": price, "ema_price": ema_price, - "vaa": vaa, + "update_data": [update_data], } return price_feed + + @staticmethod + def extract_price_feed_v2(data: dict) -> list[PriceFeed]: + """ + Extracts PriceFeed objects from the v2 JSON response (multiple price feeds) from Hermes. + """ + update_data = data["binary"]["data"] - async def get_pyth_prices_latest(self, feedIds: list[str]) -> list[PriceFeed]: + price_feeds = [] + + for feed in data["parsed"]: + price = Price.from_dict(feed["price"]) + ema_price = Price.from_dict(feed["ema_price"]) + price_feed = { + "feed_id": feed["id"], + "price": price, + "ema_price": ema_price, + "update_data": update_data, + } + price_feeds.append(price_feed) + + return price_feeds + + async def get_pyth_prices_latest(self, feedIds: list[str], version=2) -> list[PriceFeed]: """ Queries the Hermes https endpoint for the latest price feeds for a list of Pyth feed IDs. """ - url = os.path.join(self.endpoint, "latest_price_feeds?") - params = {"ids[]": feedIds, "binary": "true"} + if version==1: + url = os.path.join(self.endpoint, "api/latest_price_feeds") + params = {"ids[]": feedIds, "binary": "true"} + elif version==2: + url = os.path.join(self.endpoint, "v2/updates/price/latest") + params = {"ids[]": feedIds, "encoding": "base64", "parsed": "true"} + else: + parse_unsupported_version(version) data = (await self.client.get(url, params=params)).json() - results = [] - for res in data: - price_feed = self.extract_price_feed(res) - results.append(price_feed) + if version==1: + results = [] + for res in data: + price_feed = self.extract_price_feed_v1(res) + results.append(price_feed) + elif version==2: + results = self.extract_price_feed_v2(data) return results - async def get_pyth_price_at_time(self, feed_id: str, timestamp: int) -> PriceFeed: + async def get_pyth_price_at_time(self, feed_id: str, timestamp: int, version=2) -> PriceFeed: """ Queries the Hermes https endpoint for the price feed for a Pyth feed ID at a given timestamp. """ - url = os.path.join(self.endpoint, "get_price_feed") - params = {"id": feed_id, "publish_time": timestamp, "binary": "true"} + if version==1: + url = os.path.join(self.endpoint, "api/get_price_feed") + params = {"id": feed_id, "publish_time": timestamp, "binary": "true"} + elif version==2: + url = os.path.join(self.endpoint, f"v2/updates/price/{timestamp}") + params = {"ids[]": [feed_id], "encoding": "base64", "parsed": "true"} + else: + parse_unsupported_version(version) data = (await self.client.get(url, params=params)).json() - price_feed = self.extract_price_feed(data) + if version==1: + price_feed = self.extract_price_feed_v1(data) + elif version==2: + price_feed = self.extract_price_feed_v2(data)[0] return price_feed - async def get_all_prices(self) -> dict[str, PriceFeed]: + async def get_all_prices(self, version=2) -> dict[str, PriceFeed]: """ Queries the Hermes http endpoint for the latest price feeds for all feed IDs in the class object. @@ -98,22 +144,21 @@ async def get_all_prices(self) -> dict[str, PriceFeed]: """ pyth_prices_latest = [] i = 0 - batch_size = 100 - while len(self.feed_ids[i : i + batch_size]) > 0: + while len(self.feed_ids[i : i + self.feed_batch_size]) > 0: pyth_prices_latest += await self.get_pyth_prices_latest( - self.feed_ids[i : i + batch_size] + self.feed_ids[i : i + self.feed_batch_size], + version=version, ) - i += batch_size + i += self.feed_batch_size return dict([(feed['feed_id'], feed) for feed in pyth_prices_latest]) - async def ws_pyth_prices(self): + async def ws_pyth_prices(self, version=1): """ Opens a websocket connection to Hermes for latest prices for all feed IDs in the class object. """ - import json - - import websockets + if version != 1: + parse_unsupported_version(version) async with websockets.connect(self.ws_endpoint) as ws: while True: @@ -139,30 +184,32 @@ async def ws_pyth_prices(self): feed_id = msg["price_feed"]["id"] new_feed = msg["price_feed"] - self.prices_dict[feed_id] = self.extract_price_feed(new_feed) + self.prices_dict[feed_id] = self.extract_price_feed_v1(new_feed) - except: - raise Exception("Error in price_update message", msg) + except Exception as e: + raise Exception(f"Error in price_update message: {msg}") from e async def main(): hermes_client = HermesClient([]) feed_ids = await hermes_client.get_price_feed_ids() feed_ids_rel = feed_ids[:50] + version = 2 hermes_client.add_feed_ids(feed_ids_rel) - - prices_latest = await hermes_client.get_pyth_prices_latest(feed_ids_rel) + + prices_latest = await hermes_client.get_pyth_prices_latest(feed_ids[:50], version=version) try: - price_at_time = await hermes_client.get_pyth_price_at_time(feed_ids[0], 1_700_000_000) + price_at_time = await hermes_client.get_pyth_price_at_time(feed_ids[0], 1_700_000_000, version=version) + print(price_at_time) except Exception as e: print(f"Error in get_pyth_price_at_time, {e}") - all_prices = await hermes_client.get_all_prices() + all_prices = await hermes_client.get_all_prices(version=version) print("Starting web socket...") - ws_call = hermes_client.ws_pyth_prices() + ws_call = hermes_client.ws_pyth_prices(version=version) asyncio.create_task(ws_call) while True: diff --git a/tests/test_hermes.py b/tests/test_hermes.py index 45b8ab3..40d9164 100644 --- a/tests/test_hermes.py +++ b/tests/test_hermes.py @@ -1,15 +1,107 @@ +import pytest + +from pytest_mock import MockerFixture + +from mock import AsyncMock + from pythclient.hermes import HermesClient, PriceFeed -BTC_ID = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" # BTC/USD -ETH_ID = "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace" # ETH/USD +@pytest.fixture +def feed_ids(): + return ["e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"] + +@pytest.fixture +def hermes_client(feed_ids): + return HermesClient(feed_ids) + +@pytest.fixture +def data_v1(): + return { + "ema_price": { + "conf": "509500001", + "expo": -8, + "price": "2920679499999", + "publish_time": 1708363256 + }, + "id": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", + "metadata": { + "emitter_chain": 26, + "prev_publish_time": 1708363256, + "price_service_receive_time": 1708363256, + "slot": 85480034 + }, + "price": { + "conf": "509500001", + "expo": -8, + "price": "2920679499999", + "publish_time": 1708363256 + }, + "vaa": "UE5BVQEAAAADuAEAAAADDQC1H7meY5fTed0FsykIb8dt+7nKpbuzfvU2DplDi+dcUl8MC+UIkS65+rkiq+zmNBxE2gaxkBkjdIicZ/fBo+X7AAEqp+WtlWb84np8jJfLpuQ2W+l5KXTigsdAhz5DyVgU3xs+EnaIZxBwcE7EKzjMam+V9rlRy0CGsiQ1kjqqLzfAAQLsoVO0Vu5gVmgc8XGQ7xYhoz36rsBgMjG+e3l/B01esQi/KzPuBf/Ar8Sg5aSEOvEU0muSDb+KIr6d8eEC+FtcAAPZEaBSt4ysXVL84LUcJemQD3SiG30kOfUpF8o7/wI2M2Jf/LyCsbKEQUyLtLbZqnJBSfZJR5AMsrnHDqngMLEGAAY4UDG9GCpRuPvg8hOlsrXuPP3zq7yVPqyG0SG+bNo8rEhP5b1vXlHdG4bZsutX47d5VZ6xnFROKudx3T3/fnWUAQgAU1+kUFc3e0ZZeX1dLRVEryNIVyxMQIcxWwdey+jlIAYowHRM0fJX3Scs80OnT/CERwh5LMlFyU1w578NqxW+AQl2E/9fxjgUTi8crOfDpwsUsmOWw0+Q5OUGhELv/2UZoHAjsaw9OinWUggKACo4SdpPlHYldoWF+J2yGWOW+F4iAQre4c+ocb6a9uSWOnTldFkioqhd9lhmV542+VonCvuy4Tu214NP+2UNd/4Kk3KJCf3iziQJrCBeLi1cLHdLUikgAQtvRFR/nepcF9legl+DywAkUHi5/1MNjlEQvlHyh2XbMiS85yu7/9LgM6Sr+0ukfZY5mSkOcvUkpHn+T+Nw/IrQAQ7lty5luvKUmBpI3ITxSmojJ1aJ0kj/dc0ZcQk+/qo0l0l3/eRLkYjw5j+MZKA8jEubrHzUCke98eSoj8l08+PGAA+DAKNtCwNZe4p6J1Ucod8Lo5RKFfA84CPLVyEzEPQFZ25U9grUK6ilF4GhEia/ndYXLBt3PGW3qa6CBBPM7rH3ABGAyYEtUwzB4CeVedA5o6cKpjRkIebqDNSOqltsr+w7kXdfFVtsK2FMGFZNt5rbpIR+ppztoJ6eOKHmKmi9nQ99ARKkTxRErOs9wJXNHaAuIRV38o1pxRrlQRzGsRuKBqxcQEpC8OPFpyKYcp6iD5l7cO/gRDTamLFyhiUBwKKMP07FAWTEJv8AAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAAAGp0GAUFVV1YAAAAAAAUYUmIAACcQBsfKUtr4PgZbIXRxRESU79PjE4IBAFUA5i32yLSoX+GmfbRNwS3l2zMPesZrctxliv7fD0pBW0MAAAKqqMJFwAAAAAAqE/NX////+AAAAABkxCb7AAAAAGTEJvoAAAKqIcWxYAAAAAAlR5m4CP/mPsh1IezjYpDlJ4GRb5q4fTs2LjtyO6M0XgVimrIQ4kSh1qg7JKW4gbGkyRntVFR9JO/GNd3FPDit0BK6M+JzXh/h12YNCz9wxlZTvXrNtWNbzqT+91pvl5cphhSPMfAHyEzTPaGR9tKDy9KNu56pmhaY32d2vfEWQmKo22guegeR98oDxs67MmnUraco46a3zEnac2Bm80pasUgMO24=" + } + +@pytest.fixture +def data_v2(): + return { + "binary": { + "encoding": "hex", + "data": [ + "504e41550100000003b801000000030d014016474bab1868acfe943cdcd3cf7a8b7ccfaf6f2a31870694d11c441505d0552a42f57df50093df73eca16fad7ae3d768b0dd0e64dbaf71579fd5d05c46a5f20002098e46154c00ee17e878295edaca5decd18f7a1e9a1f0576ca090219f350118d1a4a0cc94b853c8ae1d5064439e719c953e61450745cf10086c37ec93d878b610003edf89d49fe5bb035d3cab5f3960ca5c7be37073b6680afb0f154ec684990923330f6db1fced4680dcfce8664c9d757fe2e8ca84aec8950004371ab794979db7101068a0231af6701f5fbfe55ac7dd31d640dd17f2fa92a10450d7a6e5db03c7c1f90131452ed1e3290fbbf00bc8528f616e81771460b2c307e02db811a84545180620107ab6ea34d72541f44cf34c8e919b9ef336eef9774ee4cf3d5c7cc71f5f90e49d23a05878e2e278402aff8217df84f9ce3ae782c389b3230d09e9e66fada355d6600084018b5993c68c4d616a570925e63a7c82c5444aee9a0f6153bd724e0755d3086374c9cf4e6ec2f08ab9c914b4cd3868e250ad4f946219cc2af0a31936cd38147000a079d8fb93db9c82263556dfd768b6173da40d35ea4691d21de59cf207381b5a05cb693fd4a75cb2b190c0270f2ddc14335adca66bcd5a634bf316a4385e97250010bf6dfa12e7820c58514c74ec21029d5c11f98a586743b2da9d2e20d8d78b44bd3730af5c6428c1ad865cb9d94ee795d059b3b51bb1e7bc8f81d52e5db18167648010c8558ac8aefd43cf489bce260afaee270d36fd1a34923439261fc8220cb33f30521cfefebfe0d7cf21d3aaa60c9149f8ab085c90b0509ad2850efe01fc618ccec010d6bc67036011a75277ca476ca1f4d962ca0d861805a94c6353ad0ff6ae17263bc5401e7d7ee3f3010f77c6349ff264c4185b167f32108c7de9743f7a258c62d03000e63f823f4b8f2cb1d158aac8f7ba0e45227b6d99106831a50729825bf8b97969503f55bc33778ef6c21e696a99d304b72c9e5ca3941dd178a7fc5367aed7d0e00010f22ccd76becc94aec99ff0bb1fce128cb25644268c65ac8f2bf5fe357910942381e184a62e8a768d5be200e21e40a34047a6e5cd981d2380de7eb4aa46a15ce0a00127957a07e69f8af6f8752a09f24dde0d43277c80d3bc24f09a281e5e68878d0ea0445b356257e25e80422ddff2f799bb732eafdeee43fc14c21d4eda349a547010165d38df800000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa7100000000027a3abd0141555756000000000007823fd000002710b939e515de35dd00cf7feaba6be6aed77c83e09901005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000004b868a1a543000000009ea4861cfffffff80000000065d38df80000000065d38df7000004bcd90bec4000000000c41abcc80ab559eef775bd945c821d89ceba075f3c60f2dba713f2f7ed0d210ea03ee4bead9c9b6ffd8fff45f0826e6950c44a8a7e0eac9b5bc1f2bdf276965107fc612f72a05bd37ca85017dc13b01fa5d434887f33527d87c34f1caf4ed69501a6972959e7faf96a6bc43c0d08e2b1a095c50ef6609bf81b7661102f69acb46430115e301f1ebda0f008438e31564240e1cbc9092db20b73bfc8dd832b6467fd242f0043a167ccafbc0ba479d38be012ad1d75f35e2681754e78e1f10096a55f65512fe381238a67ffce0970" + ] + }, + "parsed": [ + { + "id": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", + "price": { + "price": "5190075917635", + "conf": "2661582364", + "expo": -8, + "publish_time": 1708363256 + }, + "ema_price": { + "price": "5209141800000", + "conf": "3290086600", + "expo": -8, + "publish_time": 1708363256 + }, + "metadata": { + "slot": 125976528, + "proof_available_time": 1708363257, + "prev_publish_time": 1708363255 + } + } + ] + } + +@pytest.fixture +def mock_get_price_feed_ids(mocker: MockerFixture): + async_mock = AsyncMock() + mocker.patch('pythclient.hermes.HermesClient.get_price_feed_ids', side_effect=async_mock) + return async_mock + +@pytest.mark.asyncio +async def test_hermes_add_feed_ids(hermes_client: HermesClient): + mock_get_price_feed_ids.return_value = ["ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"] + + feed_ids = hermes_client.get_price_feed_ids() + + feed_ids_pre = hermes_client.feed_ids + pending_feed_ids_pre = hermes_client.pending_feed_ids + + hermes_client.add_feed_ids(feed_ids) + + assert hermes_client.feed_ids == list(set(feed_ids_pre + feed_ids)) + assert hermes_client.pending_feed_ids == list(set(pending_feed_ids_pre + feed_ids)) + + -async def test_hermes_return_price_feed_object(): - # Test that the hermes get request returns a dict with same keys as PriceFeed - hermes_client = HermesClient([]) - hermes_client.add_feed_ids([BTC_ID, ETH_ID]) +def test_hermes_extract_price_feed_v1(hermes_client: HermesClient, data_v1: dict): + price_feed = hermes_client.extract_price_feed_v1(data_v1) - all_prices = await hermes_client.get_all_prices() + assert isinstance(price_feed, dict) + assert set(price_feed.keys()) == set(PriceFeed.__annotations__.keys()) + +def test_hermes_extract_price_feed_v2(data_v2: dict): + price_feed = hermes_client.extract_price_feed_v2(data_v2) - assert isinstance(all_prices, dict) - assert set(all_prices[BTC_ID].keys()) == set(PriceFeed.__annotations__.keys()) - assert set(all_prices[ETH_ID].keys()) == set(PriceFeed.__annotations__.keys()) \ No newline at end of file + assert isinstance(price_feed, dict) + assert set(price_feed.keys()) == set(PriceFeed.__annotations__.keys()) From c22f8ba99bcc724501b58b4c1a82272518518b45 Mon Sep 17 00:00:00 2001 From: ani Date: Tue, 20 Feb 2024 12:55:49 -0500 Subject: [PATCH 04/10] dep add --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4ec4447..973b29b 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -requirements = ['aiodns', 'aiohttp>=3.7.4', 'backoff', 'base58', 'flake8', 'loguru', 'typing-extensions', 'pytz', 'pycryptodome', 'httpx'] +requirements = ['aiodns', 'aiohttp>=3.7.4', 'backoff', 'base58', 'flake8', 'loguru', 'typing-extensions', 'pytz', 'pycryptodome', 'httpx', 'websockets', 'json'] with open('README.md', 'r', encoding='utf-8') as fh: long_description = fh.read() From 0faca3df5152a4d36e29777282369c89890fc329 Mon Sep 17 00:00:00 2001 From: ani Date: Tue, 20 Feb 2024 13:07:23 -0500 Subject: [PATCH 05/10] dep fix --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 973b29b..afdc037 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -requirements = ['aiodns', 'aiohttp>=3.7.4', 'backoff', 'base58', 'flake8', 'loguru', 'typing-extensions', 'pytz', 'pycryptodome', 'httpx', 'websockets', 'json'] +requirements = ['aiodns', 'aiohttp>=3.7.4', 'backoff', 'base58', 'flake8', 'loguru', 'typing-extensions', 'pytz', 'pycryptodome', 'httpx', 'websockets'] with open('README.md', 'r', encoding='utf-8') as fh: long_description = fh.read() From 6d1d4d8ee40ac41954e62e7e33c87da6cb3cc3eb Mon Sep 17 00:00:00 2001 From: ani Date: Tue, 20 Feb 2024 13:12:30 -0500 Subject: [PATCH 06/10] fix an arg error --- tests/test_hermes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_hermes.py b/tests/test_hermes.py index 40d9164..7751381 100644 --- a/tests/test_hermes.py +++ b/tests/test_hermes.py @@ -82,7 +82,7 @@ def mock_get_price_feed_ids(mocker: MockerFixture): async def test_hermes_add_feed_ids(hermes_client: HermesClient): mock_get_price_feed_ids.return_value = ["ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"] - feed_ids = hermes_client.get_price_feed_ids() + feed_ids = await hermes_client.get_price_feed_ids() feed_ids_pre = hermes_client.feed_ids pending_feed_ids_pre = hermes_client.pending_feed_ids @@ -100,7 +100,7 @@ def test_hermes_extract_price_feed_v1(hermes_client: HermesClient, data_v1: dict assert isinstance(price_feed, dict) assert set(price_feed.keys()) == set(PriceFeed.__annotations__.keys()) -def test_hermes_extract_price_feed_v2(data_v2: dict): +def test_hermes_extract_price_feed_v2(hermes_client: HermesClient, data_v2: dict): price_feed = hermes_client.extract_price_feed_v2(data_v2) assert isinstance(price_feed, dict) From 7036e70310920cbc7a82a81c7a6d7a72358b7842 Mon Sep 17 00:00:00 2001 From: ani Date: Tue, 20 Feb 2024 14:57:40 -0500 Subject: [PATCH 07/10] fixed tests and cleaned up --- examples/read_hermes.py | 17 +++++++------- pythclient/hermes.py | 40 +++++---------------------------- setup.py | 2 +- tests/test_hermes.py | 49 ++++++++++++++++++++--------------------- 4 files changed, 39 insertions(+), 69 deletions(-) diff --git a/examples/read_hermes.py b/examples/read_hermes.py index 325c2df..e9b9575 100644 --- a/examples/read_hermes.py +++ b/examples/read_hermes.py @@ -8,28 +8,29 @@ async def get_hermes_prices(): hermes_client = HermesClient([]) feed_ids = await hermes_client.get_price_feed_ids() feed_ids_rel = feed_ids[:2] - version = 1 + version_http = 2 + version_ws = 1 hermes_client.add_feed_ids(feed_ids_rel) - prices_latest = await hermes_client.get_all_prices(version=version) + prices_latest = await hermes_client.get_all_prices(version=version_http) sd = list(prices_latest.keys())[0] for feed_id, price_feed in prices_latest.items(): print("Initial prices") - price_latest = price_feed["price"].price - conf_latest = price_feed["price"].conf - print(f"Feed ID: {feed_id}, Price: {price_latest}, Confidence: {conf_latest}, Time: {price_feed['price'].publish_time}") + print(f"Feed ID: {feed_id}, Price: {price_feed['price'].price}, Confidence: {price_feed['price'].conf}, Time: {price_feed['price'].publish_time}") print("Starting web socket...") - ws_call = hermes_client.ws_pyth_prices(version=version) - asyncio.create_task(ws_call) + ws_call = hermes_client.ws_pyth_prices(version=version_ws) + ws_task = asyncio.create_task(ws_call) while True: await asyncio.sleep(5) + if ws_task.done(): + break print("Latest prices:") for feed_id, price_feed in hermes_client.prices_dict.items(): - print(f"Feed ID: {feed_id}, Price: {price_latest}, Confidence: {conf_latest}, Time: {price_feed['price'].publish_time}") + print(f"Feed ID: {feed_id}, Price: {price_feed['price'].price}, Confidence: {price_feed['price'].conf}, Time: {price_feed['price'].publish_time}") asyncio.run(get_hermes_prices()) diff --git a/pythclient/hermes.py b/pythclient/hermes.py index 657d3e7..899aab9 100644 --- a/pythclient/hermes.py +++ b/pythclient/hermes.py @@ -33,7 +33,7 @@ def __init__(self, feed_ids: list[str], endpoint=HERMES_ENDPOINT_HTTPS, ws_endpo self.client = httpx.AsyncClient() self.endpoint = endpoint self.ws_endpoint = ws_endpoint - self.feed_batch_size = feed_batch_size + self.feed_batch_size = feed_batch_size # max number of feed IDs to query at once in https requests async def get_price_feed_ids(self) -> list[str]: """ @@ -70,7 +70,7 @@ def extract_price_feed_v1(data: dict) -> PriceFeed: @staticmethod def extract_price_feed_v2(data: dict) -> list[PriceFeed]: """ - Extracts PriceFeed objects from the v2 JSON response (multiple price feeds) from Hermes. + Extracts PriceFeed objects from the v2 JSON response (array of price feeds) from Hermes. """ update_data = data["binary"]["data"] @@ -140,7 +140,7 @@ async def get_all_prices(self, version=2) -> dict[str, PriceFeed]: """ Queries the Hermes http endpoint for the latest price feeds for all feed IDs in the class object. - There are limitations on the number of feed IDs that can be queried at once, so this function queries the feed IDs in batches. + There is a limit on the number of feed IDs that can be queried at once, so this function queries the feed IDs in batches. """ pyth_prices_latest = [] i = 0 @@ -159,7 +159,7 @@ async def ws_pyth_prices(self, version=1): """ if version != 1: parse_unsupported_version(version) - + async with websockets.connect(self.ws_endpoint) as ws: while True: # add new price feed ids to the ws subscription @@ -187,34 +187,4 @@ async def ws_pyth_prices(self, version=1): self.prices_dict[feed_id] = self.extract_price_feed_v1(new_feed) except Exception as e: - raise Exception(f"Error in price_update message: {msg}") from e - - -async def main(): - hermes_client = HermesClient([]) - feed_ids = await hermes_client.get_price_feed_ids() - feed_ids_rel = feed_ids[:50] - version = 2 - - hermes_client.add_feed_ids(feed_ids_rel) - - prices_latest = await hermes_client.get_pyth_prices_latest(feed_ids[:50], version=version) - - try: - price_at_time = await hermes_client.get_pyth_price_at_time(feed_ids[0], 1_700_000_000, version=version) - print(price_at_time) - except Exception as e: - print(f"Error in get_pyth_price_at_time, {e}") - - all_prices = await hermes_client.get_all_prices(version=version) - - print("Starting web socket...") - ws_call = hermes_client.ws_pyth_prices(version=version) - asyncio.create_task(ws_call) - - while True: - await asyncio.sleep(1) - - -if __name__ == "__main__": - asyncio.run(main()) + raise Exception(f"Error in price_update message: {msg}") from e \ No newline at end of file diff --git a/setup.py b/setup.py index afdc037..1d43eb3 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pythclient', - version='0.1.22', + version='0.1.23', packages=['pythclient'], author='Pyth Developers', author_email='contact@pyth.network', diff --git a/tests/test_hermes.py b/tests/test_hermes.py index 7751381..ebaaee1 100644 --- a/tests/test_hermes.py +++ b/tests/test_hermes.py @@ -50,24 +50,24 @@ def data_v2(): }, "parsed": [ { - "id": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", - "price": { - "price": "5190075917635", - "conf": "2661582364", - "expo": -8, - "publish_time": 1708363256 - }, - "ema_price": { - "price": "5209141800000", - "conf": "3290086600", - "expo": -8, - "publish_time": 1708363256 - }, - "metadata": { - "slot": 125976528, - "proof_available_time": 1708363257, - "prev_publish_time": 1708363255 - } + "id": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", + "price": { + "price": "5190075917635", + "conf": "2661582364", + "expo": -8, + "publish_time": 1708363256 + }, + "ema_price": { + "price": "5209141800000", + "conf": "3290086600", + "expo": -8, + "publish_time": 1708363256 + }, + "metadata": { + "slot": 125976528, + "proof_available_time": 1708363257, + "prev_publish_time": 1708363255 + } } ] } @@ -79,7 +79,7 @@ def mock_get_price_feed_ids(mocker: MockerFixture): return async_mock @pytest.mark.asyncio -async def test_hermes_add_feed_ids(hermes_client: HermesClient): +async def test_hermes_add_feed_ids(hermes_client: HermesClient, mock_get_price_feed_ids: AsyncMock): mock_get_price_feed_ids.return_value = ["ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"] feed_ids = await hermes_client.get_price_feed_ids() @@ -89,10 +89,8 @@ async def test_hermes_add_feed_ids(hermes_client: HermesClient): hermes_client.add_feed_ids(feed_ids) - assert hermes_client.feed_ids == list(set(feed_ids_pre + feed_ids)) - assert hermes_client.pending_feed_ids == list(set(pending_feed_ids_pre + feed_ids)) - - + assert set(hermes_client.feed_ids) == set(feed_ids_pre + feed_ids) + assert set(hermes_client.pending_feed_ids) == set(pending_feed_ids_pre + feed_ids) def test_hermes_extract_price_feed_v1(hermes_client: HermesClient, data_v1: dict): price_feed = hermes_client.extract_price_feed_v1(data_v1) @@ -103,5 +101,6 @@ def test_hermes_extract_price_feed_v1(hermes_client: HermesClient, data_v1: dict def test_hermes_extract_price_feed_v2(hermes_client: HermesClient, data_v2: dict): price_feed = hermes_client.extract_price_feed_v2(data_v2) - assert isinstance(price_feed, dict) - assert set(price_feed.keys()) == set(PriceFeed.__annotations__.keys()) + assert isinstance(price_feed, list) + assert isinstance(price_feed[0], dict) + assert set(price_feed[0].keys()) == set(PriceFeed.__annotations__.keys()) From 6fdcec0aa7a17a82f38f78993ff71bd67d8b58f6 Mon Sep 17 00:00:00 2001 From: ani Date: Tue, 20 Feb 2024 15:20:15 -0500 Subject: [PATCH 08/10] better context management --- pythclient/hermes.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pythclient/hermes.py b/pythclient/hermes.py index 899aab9..03da2de 100644 --- a/pythclient/hermes.py +++ b/pythclient/hermes.py @@ -30,7 +30,6 @@ def __init__(self, feed_ids: list[str], endpoint=HERMES_ENDPOINT_HTTPS, ws_endpo self.feed_ids = feed_ids self.pending_feed_ids = feed_ids self.prices_dict: dict[str, PriceFeed] = {} - self.client = httpx.AsyncClient() self.endpoint = endpoint self.ws_endpoint = ws_endpoint self.feed_batch_size = feed_batch_size # max number of feed IDs to query at once in https requests @@ -42,7 +41,8 @@ async def get_price_feed_ids(self) -> list[str]: url = os.path.join(self.endpoint, "api/price_feed_ids") - data = (await self.client.get(url)).json() + async with httpx.AsyncClient() as client: + data = (await client.get(url)).json() return data @@ -102,7 +102,8 @@ async def get_pyth_prices_latest(self, feedIds: list[str], version=2) -> list[Pr else: parse_unsupported_version(version) - data = (await self.client.get(url, params=params)).json() + async with httpx.AsyncClient() as client: + data = (await client.get(url, params=params)).json() if version==1: results = [] @@ -127,7 +128,8 @@ async def get_pyth_price_at_time(self, feed_id: str, timestamp: int, version=2) else: parse_unsupported_version(version) - data = (await self.client.get(url, params=params)).json() + async with httpx.AsyncClient() as client: + data = (await client.get(url, params=params)).json() if version==1: price_feed = self.extract_price_feed_v1(data) From 41426bea9fa7a2c87323f40d24f950dcff4f5adf Mon Sep 17 00:00:00 2001 From: ani Date: Wed, 21 Feb 2024 10:13:04 -0500 Subject: [PATCH 09/10] increase coverage a bit --- examples/read_hermes.py | 2 -- tests/test_hermes.py | 13 +++++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/read_hermes.py b/examples/read_hermes.py index e9b9575..a732e60 100644 --- a/examples/read_hermes.py +++ b/examples/read_hermes.py @@ -15,8 +15,6 @@ async def get_hermes_prices(): prices_latest = await hermes_client.get_all_prices(version=version_http) - sd = list(prices_latest.keys())[0] - for feed_id, price_feed in prices_latest.items(): print("Initial prices") print(f"Feed ID: {feed_id}, Price: {price_feed['price'].price}, Confidence: {price_feed['price'].conf}, Time: {price_feed['price'].publish_time}") diff --git a/tests/test_hermes.py b/tests/test_hermes.py index ebaaee1..25b1bf5 100644 --- a/tests/test_hermes.py +++ b/tests/test_hermes.py @@ -4,7 +4,7 @@ from mock import AsyncMock -from pythclient.hermes import HermesClient, PriceFeed +from pythclient.hermes import HermesClient, PriceFeed, parse_unsupported_version @pytest.fixture def feed_ids(): @@ -72,6 +72,12 @@ def data_v2(): ] } +def test_parse_unsupported_version(): + with pytest.raises(ValueError): + parse_unsupported_version(3) + with pytest.raises(TypeError): + parse_unsupported_version("3") + @pytest.fixture def mock_get_price_feed_ids(mocker: MockerFixture): async_mock = AsyncMock() @@ -87,8 +93,11 @@ async def test_hermes_add_feed_ids(hermes_client: HermesClient, mock_get_price_f feed_ids_pre = hermes_client.feed_ids pending_feed_ids_pre = hermes_client.pending_feed_ids - hermes_client.add_feed_ids(feed_ids) + # Add feed_ids to the client in duplicate + for _ in range(3): + hermes_client.add_feed_ids(feed_ids) + assert len(set(hermes_client.feed_ids)) == len(hermes_client.feed_ids) assert set(hermes_client.feed_ids) == set(feed_ids_pre + feed_ids) assert set(hermes_client.pending_feed_ids) == set(pending_feed_ids_pre + feed_ids) From ab9149a35c5cc1bcf381e6f14469ffe007523d57 Mon Sep 17 00:00:00 2001 From: ani Date: Wed, 21 Feb 2024 11:18:55 -0500 Subject: [PATCH 10/10] add more test coverage, for socket cases --- examples/read_hermes.py | 2 +- tests/test_hermes.py | 121 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 112 insertions(+), 11 deletions(-) diff --git a/examples/read_hermes.py b/examples/read_hermes.py index a732e60..62ced85 100644 --- a/examples/read_hermes.py +++ b/examples/read_hermes.py @@ -8,7 +8,7 @@ async def get_hermes_prices(): hermes_client = HermesClient([]) feed_ids = await hermes_client.get_price_feed_ids() feed_ids_rel = feed_ids[:2] - version_http = 2 + version_http = 1 version_ws = 1 hermes_client.add_feed_ids(feed_ids_rel) diff --git a/tests/test_hermes.py b/tests/test_hermes.py index 25b1bf5..ce9c741 100644 --- a/tests/test_hermes.py +++ b/tests/test_hermes.py @@ -4,11 +4,17 @@ from mock import AsyncMock +import httpx + from pythclient.hermes import HermesClient, PriceFeed, parse_unsupported_version +@pytest.fixture +def feed_id(): + return "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" + @pytest.fixture def feed_ids(): - return ["e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"] + return ["e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"] @pytest.fixture def hermes_client(feed_ids): @@ -72,11 +78,25 @@ def data_v2(): ] } -def test_parse_unsupported_version(): - with pytest.raises(ValueError): - parse_unsupported_version(3) - with pytest.raises(TypeError): - parse_unsupported_version("3") +@pytest.fixture +def json_get_price_feed_ids(): + return ["e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"] + +@pytest.fixture +def json_get_pyth_prices_latest_v1(feed_ids: list[str]): + return [{'id': feed_ids[0], 'price': {'price': '135077', 'conf': '69', 'expo': -5, 'publish_time': 1708529661}, 'ema_price': {'price': '135182', 'conf': '52', 'expo': -5, 'publish_time': 1708529661}, 'vaa': 'UE5BVQEAAAADuAEAAAADDQFOmUMU+Z4r00C4N2LLbib67MfjUQrRDE9JCMD2rRAbHw/wKlgC1RgVuom5U+XO3mTFBUySDPxVLdKZT2+kSPNPAQLUFqHSI4KJPqeD0kt+JojinHUrlMFPw6y/tq9Go/izICpUhJY0+RUoQQ1it5jJKhKIe0GOVCWDN3M8WZwjDNW1AQTpTokfNehR1p7GQdWOCoEiec0ARzKkAHYkIJAOaHGUOhXrvL2g2P/nXZzBK32a67vI2ia1e/sZN0rLclEobzDgAAbGveTKWcp9znmPAPAA1mGf+ZeJy7eXN/9vWrdEZc2wQ1cRO0eYAUcMMLAKNJ80lTKN7yxn0sekTUN1NSDdOSctAAdm8xF1ucpBgaOK7vo+OoXZa2nJ5vNxbUM99qvVxeg6sCMqwVD03/BD2VeuUxGGJUIJgbi6RViZoEahk2GcTE5kAQiwuLqaPQ8YJgYJ2ODBVZHXzTosLVWpxJQr1RwCvBpNKTiBFX6TMXJk6cWgKpi0siRFkMrMXuVApOMUbXtFGHmoAQoyfR9lYa/G8reoSK/NUaN2eNWgEpODlNlVRPSC1pe3YRG7BaGMyyOLc4dF3NDO/Mv8GERLRnq/5uk6hZmfMrdaAQsdCecWyLR6ajfuhZQXqXvSKYlkLum6WmWh1Rgytaiq3lXIQ0KzNxplogC7/uDxaICmwFMQIe5j+0vkQjK0YRtkAAwPqtN22f08AAFJ9vkLJ2EHNrzRqgpii3hGM4B+lqipvCT1Zwrrc4Akm2cT8jQvta08zHOGA9upZuyRefMKtvIrAQ0BoSlqog4qD/tABWgvxxCP0S7leutd1BtdcPkDMOyIaRt4HujJn4K4+7tqsukqkvdsm9kffRt2apxDLHpLttvCAQ4j24Btj0Z+U7soibfa/1asb/u+sDzydMZPiMEf6441IRaGdRHaeo9pCKzoaUc8xBcktFHsa7hZR8bE1xiYzCJzABB5St+Lvp/I2XXIjLB0TnscZRs/TDnRGl0N0UudLZ7BNgmtOCD8gLtvrnpSOegfeO+yEbHsAosn/CJVD1jgxuQuARLEIz8Z8yew/zDanqL+zjnR3GayU3ddKqP8AlGuxZjZNXEsQZP93ivcRgX14VyK+qxX9bHmsb/5+ljg3rNaokbyAWXWF/0AAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAACfpqyAUFVV1YAAAAAAAeGqjMAACcQiDrN+zmqsRbApltVCbB2uiQTTXkBAFUAMRKwOkHJEO1EaFKqz2cRjLG+xnss0LmiFMWMwOqi7MoAAAAAAAIPpQAAAAAAAABF////+wAAAABl1hf9AAAAAGXWF/0AAAAAAAIQDgAAAAAAAAA0Crt2twYzlx9tlARo8EHFjGit6NU5uJVQez40nh22JSTHSjgQajW5RNeQbb732lp/y/9sv9HmENJElTPq3K22/AizzBGRk8LG9r40CWoY4KWiKxi9UqTvbO8hGd/Q6XyH+vrfu68/YWiqdx50GJkhYQS/nLfF9lgMMWf+svoFsyU8hbanvEjRht/xhRD30QKgQ0XqHBBRB5x1oagos+qIqYZY6Izt6SpS/Fj4p6qUrezAAEmVz83V+LurKg3kaurFWAzvwhnsQyop'}, {'id': feed_ids[1], 'price': {'price': '939182', 'conf': '1549', 'expo': -8, 'publish_time': 1708529661}, 'ema_price': {'price': '945274', 'conf': '2273', 'expo': -8, 'publish_time': 1708529661}, 'vaa': 'UE5BVQEAAAADuAEAAAADDQFOmUMU+Z4r00C4N2LLbib67MfjUQrRDE9JCMD2rRAbHw/wKlgC1RgVuom5U+XO3mTFBUySDPxVLdKZT2+kSPNPAQLUFqHSI4KJPqeD0kt+JojinHUrlMFPw6y/tq9Go/izICpUhJY0+RUoQQ1it5jJKhKIe0GOVCWDN3M8WZwjDNW1AQTpTokfNehR1p7GQdWOCoEiec0ARzKkAHYkIJAOaHGUOhXrvL2g2P/nXZzBK32a67vI2ia1e/sZN0rLclEobzDgAAbGveTKWcp9znmPAPAA1mGf+ZeJy7eXN/9vWrdEZc2wQ1cRO0eYAUcMMLAKNJ80lTKN7yxn0sekTUN1NSDdOSctAAdm8xF1ucpBgaOK7vo+OoXZa2nJ5vNxbUM99qvVxeg6sCMqwVD03/BD2VeuUxGGJUIJgbi6RViZoEahk2GcTE5kAQiwuLqaPQ8YJgYJ2ODBVZHXzTosLVWpxJQr1RwCvBpNKTiBFX6TMXJk6cWgKpi0siRFkMrMXuVApOMUbXtFGHmoAQoyfR9lYa/G8reoSK/NUaN2eNWgEpODlNlVRPSC1pe3YRG7BaGMyyOLc4dF3NDO/Mv8GERLRnq/5uk6hZmfMrdaAQsdCecWyLR6ajfuhZQXqXvSKYlkLum6WmWh1Rgytaiq3lXIQ0KzNxplogC7/uDxaICmwFMQIe5j+0vkQjK0YRtkAAwPqtN22f08AAFJ9vkLJ2EHNrzRqgpii3hGM4B+lqipvCT1Zwrrc4Akm2cT8jQvta08zHOGA9upZuyRefMKtvIrAQ0BoSlqog4qD/tABWgvxxCP0S7leutd1BtdcPkDMOyIaRt4HujJn4K4+7tqsukqkvdsm9kffRt2apxDLHpLttvCAQ4j24Btj0Z+U7soibfa/1asb/u+sDzydMZPiMEf6441IRaGdRHaeo9pCKzoaUc8xBcktFHsa7hZR8bE1xiYzCJzABB5St+Lvp/I2XXIjLB0TnscZRs/TDnRGl0N0UudLZ7BNgmtOCD8gLtvrnpSOegfeO+yEbHsAosn/CJVD1jgxuQuARLEIz8Z8yew/zDanqL+zjnR3GayU3ddKqP8AlGuxZjZNXEsQZP93ivcRgX14VyK+qxX9bHmsb/5+ljg3rNaokbyAWXWF/0AAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAACfpqyAUFVV1YAAAAAAAeGqjMAACcQiDrN+zmqsRbApltVCbB2uiQTTXkBAFUASWAWJeGjQsH5DD/moDrgJRmRodduSA0nQVJMKQN74ooAAAAAAA5UrgAAAAAAAAYN////+AAAAABl1hf9AAAAAGXWF/0AAAAAAA5segAAAAAAAAjhCofSSh+aiBKjNaU+6ZbH8TlrF2uCFMh/5ts6kGbO5HTy0DjKjBftORAYo1rjK75a2IgA2CgrbhU7jgoohiwnjYZjFZjCdn5MN5g66N44JEVMsvQkw7xATLYHjjwTFxVNSlb0fK71JG8U3dYnQYukR2GGWekBqlOKSGf+svoFsyU8hbanvEjRht/xhRD30QKgQ0XqHBBRB5x1oagos+qIqYZY6Izt6SpS/Fj4p6qUrezAAEmVz83V+LurKg3kaurFWAzvwhnsQyop'}] + +@pytest.fixture +def json_get_pyth_prices_latest_v2(feed_ids: list[str]): + return {'binary': {'encoding': 'base64', 'data': ['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']}, 'parsed': [{'id': feed_ids[0], 'price': {'price': '345341366', 'conf': '315539', 'expo': -8, 'publish_time': 1708529433}, 'ema_price': {'price': '345109090', 'conf': '338082', 'expo': -8, 'publish_time': 1708529433}, 'metadata': {'slot': 126265515, 'proof_available_time': 1708529435, 'prev_publish_time': 1708529433}}, {'id': feed_ids[1], 'price': {'price': '39939675', 'conf': '38766', 'expo': -5, 'publish_time': 1708529433}, 'ema_price': {'price': '39935880', 'conf': '40150', 'expo': -5, 'publish_time': 1708529433}, 'metadata': {'slot': 126265515, 'proof_available_time': 1708529435, 'prev_publish_time': 1708529433}}]} + +@pytest.fixture +def json_get_pyth_price_at_time_v1(feed_id: str): + return {'id': feed_id, 'price': {'price': '135077', 'conf': '69', 'expo': -5, 'publish_time': 1708529661}, 'ema_price': {'price': '135182', 'conf': '52', 'expo': -5, 'publish_time': 1708529661}, 'vaa': 'UE5BVQEAAAADuAEAAAADDQFOmUMU+Z4r00C4N2LLbib67MfjUQrRDE9JCMD2rRAbHw/wKlgC1RgVuom5U+XO3mTFBUySDPxVLdKZT2+kSPNPAQLUFqHSI4KJPqeD0kt+JojinHUrlMFPw6y/tq9Go/izICpUhJY0+RUoQQ1it5jJKhKIe0GOVCWDN3M8WZwjDNW1AQTpTokfNehR1p7GQdWOCoEiec0ARzKkAHYkIJAOaHGUOhXrvL2g2P/nXZzBK32a67vI2ia1e/sZN0rLclEobzDgAAbGveTKWcp9znmPAPAA1mGf+ZeJy7eXN/9vWrdEZc2wQ1cRO0eYAUcMMLAKNJ80lTKN7yxn0sekTUN1NSDdOSctAAdm8xF1ucpBgaOK7vo+OoXZa2nJ5vNxbUM99qvVxeg6sCMqwVD03/BD2VeuUxGGJUIJgbi6RViZoEahk2GcTE5kAQiwuLqaPQ8YJgYJ2ODBVZHXzTosLVWpxJQr1RwCvBpNKTiBFX6TMXJk6cWgKpi0siRFkMrMXuVApOMUbXtFGHmoAQoyfR9lYa/G8reoSK/NUaN2eNWgEpODlNlVRPSC1pe3YRG7BaGMyyOLc4dF3NDO/Mv8GERLRnq/5uk6hZmfMrdaAQsdCecWyLR6ajfuhZQXqXvSKYlkLum6WmWh1Rgytaiq3lXIQ0KzNxplogC7/uDxaICmwFMQIe5j+0vkQjK0YRtkAAwPqtN22f08AAFJ9vkLJ2EHNrzRqgpii3hGM4B+lqipvCT1Zwrrc4Akm2cT8jQvta08zHOGA9upZuyRefMKtvIrAQ0BoSlqog4qD/tABWgvxxCP0S7leutd1BtdcPkDMOyIaRt4HujJn4K4+7tqsukqkvdsm9kffRt2apxDLHpLttvCAQ4j24Btj0Z+U7soibfa/1asb/u+sDzydMZPiMEf6441IRaGdRHaeo9pCKzoaUc8xBcktFHsa7hZR8bE1xiYzCJzABB5St+Lvp/I2XXIjLB0TnscZRs/TDnRGl0N0UudLZ7BNgmtOCD8gLtvrnpSOegfeO+yEbHsAosn/CJVD1jgxuQuARLEIz8Z8yew/zDanqL+zjnR3GayU3ddKqP8AlGuxZjZNXEsQZP93ivcRgX14VyK+qxX9bHmsb/5+ljg3rNaokbyAWXWF/0AAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAACfpqyAUFVV1YAAAAAAAeGqjMAACcQiDrN+zmqsRbApltVCbB2uiQTTXkBAFUAMRKwOkHJEO1EaFKqz2cRjLG+xnss0LmiFMWMwOqi7MoAAAAAAAIPpQAAAAAAAABF////+wAAAABl1hf9AAAAAGXWF/0AAAAAAAIQDgAAAAAAAAA0Crt2twYzlx9tlARo8EHFjGit6NU5uJVQez40nh22JSTHSjgQajW5RNeQbb732lp/y/9sv9HmENJElTPq3K22/AizzBGRk8LG9r40CWoY4KWiKxi9UqTvbO8hGd/Q6XyH+vrfu68/YWiqdx50GJkhYQS/nLfF9lgMMWf+svoFsyU8hbanvEjRht/xhRD30QKgQ0XqHBBRB5x1oagos+qIqYZY6Izt6SpS/Fj4p6qUrezAAEmVz83V+LurKg3kaurFWAzvwhnsQyop'} + +@pytest.fixture +def json_get_pyth_price_at_time_v2(feed_id: str): + return {'binary': {'encoding': 'base64', 'data': ['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']}, 'parsed': [{'id': feed_id, 'price': {'price': '345341366', 'conf': '315539', 'expo': -8, 'publish_time': 1708529433}, 'ema_price': {'price': '345109090', 'conf': '338082', 'expo': -8, 'publish_time': 1708529433}, 'metadata': {'slot': 126265515, 'proof_available_time': 1708529435, 'prev_publish_time': 1708529433}}]} @pytest.fixture def mock_get_price_feed_ids(mocker: MockerFixture): @@ -84,6 +104,12 @@ def mock_get_price_feed_ids(mocker: MockerFixture): mocker.patch('pythclient.hermes.HermesClient.get_price_feed_ids', side_effect=async_mock) return async_mock +def test_parse_unsupported_version(): + with pytest.raises(ValueError): + parse_unsupported_version(3) + with pytest.raises(TypeError): + parse_unsupported_version("3") + @pytest.mark.asyncio async def test_hermes_add_feed_ids(hermes_client: HermesClient, mock_get_price_feed_ids: AsyncMock): mock_get_price_feed_ids.return_value = ["ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"] @@ -108,8 +134,83 @@ def test_hermes_extract_price_feed_v1(hermes_client: HermesClient, data_v1: dict assert set(price_feed.keys()) == set(PriceFeed.__annotations__.keys()) def test_hermes_extract_price_feed_v2(hermes_client: HermesClient, data_v2: dict): - price_feed = hermes_client.extract_price_feed_v2(data_v2) + price_feeds = hermes_client.extract_price_feed_v2(data_v2) + + assert isinstance(price_feeds, list) + assert isinstance(price_feeds[0], dict) + assert set(price_feeds[0].keys()) == set(PriceFeed.__annotations__.keys()) - assert isinstance(price_feed, list) - assert isinstance(price_feed[0], dict) - assert set(price_feed[0].keys()) == set(PriceFeed.__annotations__.keys()) +@pytest.mark.asyncio +async def test_get_price_feed_ids(hermes_client: HermesClient, json_get_price_feed_ids: list[str], mocker: AsyncMock): + mocker.patch('httpx.AsyncClient.get', return_value = httpx.Response(200, json=json_get_price_feed_ids)) + result = await hermes_client.get_price_feed_ids() + + assert result == json_get_price_feed_ids + +@pytest.mark.asyncio +async def test_get_pyth_prices_latest_v1(hermes_client: HermesClient, feed_ids: list[str], json_get_pyth_prices_latest_v1: list[dict], mocker: AsyncMock): + mocker.patch('httpx.AsyncClient.get', return_value = httpx.Response(200, json=json_get_pyth_prices_latest_v1)) + result = await hermes_client.get_pyth_prices_latest(feed_ids, version=1) + + assert isinstance(result, list) + assert len(result) == len(feed_ids) + assert isinstance(result[0], dict) + assert set(result[0].keys()) == set(PriceFeed.__annotations__.keys()) + +@pytest.mark.asyncio +async def test_get_pyth_prices_latest_v2(hermes_client: HermesClient, feed_ids: list[str], json_get_pyth_prices_latest_v2: list[str], mocker: AsyncMock): + mocker.patch('httpx.AsyncClient.get', return_value = httpx.Response(200, json=json_get_pyth_prices_latest_v2)) + result = await hermes_client.get_pyth_prices_latest(feed_ids, version=2) + + assert isinstance(result, list) + assert len(result) == len(feed_ids) + assert isinstance(result[0], dict) + assert set(result[0].keys()) == set(PriceFeed.__annotations__.keys()) + +@pytest.mark.asyncio +async def test_get_pyth_prices_latest_v3(hermes_client: HermesClient, feed_ids: list[str], mocker: AsyncMock): + mocker.patch('httpx.AsyncClient.get', return_value = httpx.Response(200, json=[])) + with pytest.raises(ValueError): + await hermes_client.get_pyth_prices_latest(feed_ids, version=3) + +@pytest.mark.asyncio +async def test_get_pyth_price_at_time_v1(hermes_client: HermesClient, feed_id: str, json_get_pyth_price_at_time_v1: dict, mocker: AsyncMock): + mocker.patch('httpx.AsyncClient.get', return_value = httpx.Response(200, json=json_get_pyth_price_at_time_v1)) + result = await hermes_client.get_pyth_price_at_time(feed_id, 1708529661, version=1) + + assert isinstance(result, dict) + assert set(result.keys()) == set(PriceFeed.__annotations__.keys()) + +@pytest.mark.asyncio +async def test_get_pyth_price_at_time_v2(hermes_client: HermesClient, feed_id: str, json_get_pyth_price_at_time_v2: dict, mocker: AsyncMock): + mocker.patch('httpx.AsyncClient.get', return_value = httpx.Response(200, json=json_get_pyth_price_at_time_v2)) + result = await hermes_client.get_pyth_price_at_time(feed_id, 1708529661, version=2) + + assert isinstance(result, dict) + assert set(result.keys()) == set(PriceFeed.__annotations__.keys()) + +@pytest.mark.asyncio +async def test_get_pyth_price_at_time_v3(hermes_client: HermesClient, feed_id: str, mocker: AsyncMock): + mocker.patch('httpx.AsyncClient.get', return_value = httpx.Response(200, json=[])) + with pytest.raises(ValueError): + await hermes_client.get_pyth_price_at_time(feed_id, 1708529661, version=3) + +@pytest.mark.asyncio +async def test_get_all_prices_v1(hermes_client: HermesClient, json_get_pyth_prices_latest_v1: list[dict], mocker: AsyncMock): + mocker.patch('httpx.AsyncClient.get', return_value = httpx.Response(200, json=json_get_pyth_prices_latest_v1)) + result = await hermes_client.get_all_prices(version=1) + + assert isinstance(result, dict) + assert set(result.keys()) == set(hermes_client.feed_ids) + assert isinstance(result[hermes_client.feed_ids[0]], dict) + assert set(result[hermes_client.feed_ids[0]].keys()) == set(PriceFeed.__annotations__.keys()) + +@pytest.mark.asyncio +async def test_get_all_prices_v2(hermes_client: HermesClient, json_get_pyth_prices_latest_v2: list[dict], mocker: AsyncMock): + mocker.patch('httpx.AsyncClient.get', return_value = httpx.Response(200, json=json_get_pyth_prices_latest_v2)) + result = await hermes_client.get_all_prices(version=2) + + assert isinstance(result, dict) + assert set(result.keys()) == set(hermes_client.feed_ids) + assert isinstance(result[hermes_client.feed_ids[0]], dict) + assert set(result[hermes_client.feed_ids[0]].keys()) == set(PriceFeed.__annotations__.keys()) \ No newline at end of file