Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hermes support #54

Merged
merged 10 commits into from
Feb 21, 2024
Merged

hermes support #54

merged 10 commits into from
Feb 21, 2024

Conversation

anihamde
Copy link
Contributor

Adding hermes support to Python SDK so that users can pull verified Pyth prices for consumption on target chains

Copy link
Contributor

@cctdaniel cctdaniel left a comment

Choose a reason for hiding this comment

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

seems like the test test_hermes_return_price_feed_object failed because it attempted to use a network socket, which is blocked by pytest-socket -- I think you should mock the network request instead (unit tests should be deterministic, and independent of external factors like network availability), see example here

examples/read_hermes.py Outdated Show resolved Hide resolved
pythclient/hermes.py Outdated Show resolved Hide resolved
pythclient/hermes.py Outdated Show resolved Hide resolved
self.feed_ids = feed_ids
self.pending_feed_ids = feed_ids
self.prices_dict: dict[str, PriceFeed] = {}
self.client = httpx.AsyncClient()
Copy link
Contributor

Choose a reason for hiding this comment

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

the httpx.AsyncClient is instantiated but not explicitly closed, which could potentially lead to resource leaks, maybe use the client as a context manager to ensure it's properly closed e.g.

    async with self.client as client:
        data = (await client.get(url)).json()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The issue is that with context manager usage, any one call will close the client which makes it impossible to open again. I think using asyncio.run manages the resource leakage by closing all tasks running within the event loop whenever it's shut down: https://github.com/python/cpython/blob/c0b0c2f2015fb27db4306109b2b3781eb2057c2b/Lib/asyncio/runners.py#L64-L79

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
Copy link
Contributor

Choose a reason for hiding this comment

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

why does self.pending_feed_ids not require uniqueness?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we always call list(set(self.feed_ids)), so the feed_ids will always be unique

Copy link
Contributor

@cctdaniel cctdaniel Feb 21, 2024

Choose a reason for hiding this comment

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

it's possible for self.pending_feed_ids to contain duplicates if the same feed IDs are added more than once before they are cleared, for e.g.

  • add_feed_ids method is called with ['feed1', 'feed2']
  • before any operation that clears self.pending_feed_ids is performed, add_feed_ids is called again with ['feed2', 'feed3']
  • self.feed_ids will now contain ['feed1', 'feed2', 'feed3'] without duplicates because it's converted to a set and then back to a list
  • however, self.pending_feed_ids will contain ['feed1', 'feed2', 'feed2', 'feed3'], where 'feed2' is duplicated.

Copy link
Contributor

@cctdaniel cctdaniel Feb 21, 2024

Choose a reason for hiding this comment

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

imo you can do something like this to make it cleaner

    # convert feed_ids to a set to remove any duplicates from the input
    new_feed_ids_set = set(feed_ids)
    
    # update self.feed_ids; convert to set for union operation, then back to list
    self.feed_ids = list(set(self.feed_ids).union(new_feed_ids_set))
    
    # update self.pending_feed_ids with only those IDs that are truly new
    self.pending_feed_ids = list(set(self.pending_feed_ids).union(new_feed_ids_set))

Copy link
Contributor

Choose a reason for hiding this comment

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

another thing that we can possibly do here is to validate that the feed_ids passed in is in the format we expect
e.g.

def validate_feed_ids(self, feed_ids: list[str]):
    """
    Validates the format of feed IDs. Each ID should be a 64-character hexadecimal string,
    optionally prefixed with '0x'.
    """
    hex_pattern = re.compile(r'^0x[a-fA-F0-9]{64}$|^[a-fA-F0-9]{64}$')
    for feed_id in feed_ids:
        if not hex_pattern.match(feed_id):
            raise ValueError(f"Invalid feed ID format: {feed_id}")

and then call validate_feed_ids in the first line of add_feed_ids and write tests to ensure that invalid ids (e.g. 30 chars, invalid chars, etc) throw an error

pythclient/hermes.py Outdated Show resolved Hide resolved
pythclient/hermes.py Outdated Show resolved Hide resolved
Comment on lines 114 to 116
import json

import websockets
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a specific reason to do this? otherwise I recommend to import it at top-level to improve readability and maintainability

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 think we did this so that we didn't import these when we were using non-websocket client--i think it was more relevant where we had the code before, I think here it's def better to move to top

Comment on lines 13 to 17
class PriceFeed(TypedDict):
feed_id: str
price: Price
ema_price: Price
vaa: str
Copy link
Contributor

@cctdaniel cctdaniel Feb 20, 2024

Choose a reason for hiding this comment

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

did you follow the v1 api schema? maybe good to implement for v2 instead since that is live already e.g. https://hermes.pyth.network/docs/#/rest/latest_price_updates
but happy to defer to @ali-bahjati on what the right action here is

Copy link
Contributor

@cctdaniel cctdaniel left a comment

Choose a reason for hiding this comment

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

left some more comments but looks good to me, another comment I have is that if you run pytest --cov-report=html you can see that the current coverage is at 43%, would be good to increase it as much as possible

examples/read_hermes.py Outdated Show resolved Hide resolved
tests/test_hermes.py Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

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

another thing that we can possibly do here is to validate that the feed_ids passed in is in the format we expect
e.g.

def validate_feed_ids(self, feed_ids: list[str]):
    """
    Validates the format of feed IDs. Each ID should be a 64-character hexadecimal string,
    optionally prefixed with '0x'.
    """
    hex_pattern = re.compile(r'^0x[a-fA-F0-9]{64}$|^[a-fA-F0-9]{64}$')
    for feed_id in feed_ids:
        if not hex_pattern.match(feed_id):
            raise ValueError(f"Invalid feed ID format: {feed_id}")

and then call validate_feed_ids in the first line of add_feed_ids and write tests to ensure that invalid ids (e.g. 30 chars, invalid chars, etc) throw an error

@anihamde anihamde merged commit 8cd3b85 into main Feb 21, 2024
1 check passed
@anihamde anihamde deleted the hermes branch February 21, 2024 16:22
@cctdaniel cctdaniel mentioned this pull request Feb 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants