-
Notifications
You must be signed in to change notification settings - Fork 195
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
Express relay python searcher #1319
Conversation
anihamde
commented
Feb 22, 2024
- Reorg Js searcher sdk
- Drop python searcher sdk (w/o websocket; to be added)
The latest updates on your projects. Learn more about Vercel for Git ↗︎ 2 Ignored Deployments
|
self.ws_msg_counter = 0 | ||
|
||
async def get_liquidation_opportunities(self) -> list[OpportunityParamsWithMetadata]: | ||
async with httpx.AsyncClient() as client: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initialize the client in the construction and use it here. Also it would be great if you can accept some config to pass into this client. For example for tuning the timeout
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can lead to some resource leakage, better to instantiate the client in the context so that it shuts down upon the end of this function call.
express_relay/sdk/python/express_relay_utils/express_relay_client.py
Outdated
Show resolved
Hide resolved
express_relay/sdk/python/express_relay_utils/express_relay_client.py
Outdated
Show resolved
Hide resolved
msg["id"] = str(self.ws_msg_counter) | ||
self.ws_msg_counter += 1 | ||
|
||
await self.ws.send(json.dumps(msg)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are not waiting for the response of this message. Server can return an error. This should be handled using Futures
express_relay/sdk/python/express_relay_utils/express_relay_client.py
Outdated
Show resolved
Hide resolved
express_relay/sdk/python/express_relay_utils/express_relay_client.py
Outdated
Show resolved
Hide resolved
express_relay/sdk/python/express_relay_utils/express_relay_client.py
Outdated
Show resolved
Hide resolved
express_relay/sdk/python/express_relay_utils/express_relay_client.py
Outdated
Show resolved
Hide resolved
express_relay/sdk/python/express_relay_utils/express_relay_client.py
Outdated
Show resolved
Hide resolved
express_relay/sdk/python/express_relay_utils/express_relay_client.py
Outdated
Show resolved
Hide resolved
express_relay/sdk/python/express_relay_utils/express_relay_client.py
Outdated
Show resolved
Hide resolved
.pre-commit-config.yaml
Outdated
@@ -4,6 +4,7 @@ repos: | |||
hooks: | |||
- id: trailing-whitespace | |||
- id: end-of-file-fixer | |||
exclude: openapi_client |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not needed anymore
.pre-commit-config.yaml
Outdated
name: black | ||
entry: poetry -C express_relay/sdk/python/express_relay_utils run black express_relay/sdk/python/express_relay_utils | ||
pass_filenames: false | ||
language: system |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does this mean?
express_relay/sdk/python/README.md
Outdated
$ poetry add express-relay-utils | ||
``` | ||
|
||
### openapi-generator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove
assert s_set.issubset(hex_set), "v must be a valid hex string" | ||
|
||
|
||
class HexString(BaseModel): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is not a natural way to access these values.
For example I expect bid.calldata
to be the actual calldata. But now I have to access it via bid.calldata.string
which is awkward. Why not use something like HexBytes
?
We can also leverage eth_typing
library for these low level types.
|
||
class Bid(BaseModel): | ||
""" | ||
Args: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these are Attributes
now
contract: Address | ||
permission_key: HexString | ||
|
||
def to_dict(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a better way to do this only by customizing the amount
field serialization instead of specifying everything.
In general defining to_dict
and from_dict
seems wrong as we are defining additional methods for serialization instead of overriding the default behavior that pydantic provides using model_dump/model_validate.
from eth_account.datastructures import SignedMessage | ||
|
||
|
||
def maybe_hex_string(s: str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe
is not a good prefix here. maybe check or validate?
opportunity = Opportunity.model_validate( | ||
msg_json.get("opportunity") | ||
) | ||
if opportunity: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think opportunity can be None here. model_validate throws an exception if invalid
warnings.warn( | ||
f"Cannot handle opportunity version: {self.version}. Please upgrade your client." | ||
) | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
according to the docs, we should either return the instance or throw an exception inside these validators. So maybe this is not the best place to handle this case. I don't know what the behavior is if we return None
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes but i think it's cleaner to handle the verification within this type. i'll add some more customized checking to throw an error but catch this while processing.
Args: | ||
opportunity_bid: An object representing the bid to submit on an opportunity. | ||
subscribe_to_updates: A boolean indicating whether to subscribe to the bid status updates. | ||
kwargs: Keyword arguments to pass to the HTTP post request. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this parameter is now a bit ill defined. If subscribe_to_updates
is True, this is useless, but we don't communicate that with the users. Anyway, I think we should reuse a single httpx client and allow the user to pass in any extra configuration required in the constructor.
) | ||
bid_id = UUID(result.get("id")) | ||
else: | ||
async with httpx.AsyncClient() as client: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I searched a bit and defining a new client per method call doesn't seem like a good idea:
https://www.python-httpx.org/advanced/clients/
I suggest creating a single client in the constructor and use it here. Also we can accept the httpx client configuration in the constructor and set it once and for all the methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the same link advises using context manager for resource management reasons. but i agree that it is better to pass it in once in the constructor, refactoring to do that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because the link does not assume we are doing this in multiple functions. Each time the user call this functions, a new client is instantiated which is suboptimal:
https://www.python-httpx.org/async/#opening-and-closing-clients
name: pyflakes | ||
entry: poetry -C express_relay/sdk/python/express_relay_utils run pyflakes express_relay/sdk/python/express_relay_utils | ||
pass_filenames: false | ||
language: "system" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't these be python
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this hook directly invokes poetry, not python, see https://sam.hooke.me/note/2023/09/poetry-pre-commit-hooks/
express_relay/sdk/python/README.md
Outdated
@@ -0,0 +1,25 @@ | |||
# Pyth Express Relay Python SDK |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove Pyth
[tool.poetry] | ||
name = "express-relay-utils" | ||
version = "0.0.1" | ||
description = "Utilities for searchers and protocols to interact with the express relay protocol." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Express Relay
|
||
return self.ws_loop | ||
|
||
async def send_ws_message(self, method: str, params: dict) -> dict: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes more sense for this function to accept a ClientMessage and the callers be responsible to craft the correct payloads
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah i wanted to avoid multiple duplicate calls to ClientMessage.model_validate
throughout the code. but i dont have a strong take on this, so can refactor it
resp.raise_for_status() | ||
|
||
opportunities = [ | ||
Opportunity.model_validate(opportunity) for opportunity in resp.json() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here is a better place to check the version before passing it to model_validate. Or we can catch the incompatible version errors here
@@ -37,14 +37,16 @@ def __init__( | |||
bid_status_callback: ( | |||
Callable[[BidStatusUpdate], Coroutine[Any, Any, Any]] | None | |||
) = None, | |||
**kwargs, | |||
ws_options: dict[str, Any] = {}, | |||
httpx_options: dict[str, Any] = {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default argument value is mutable
|
||
# validate the format of msg and construct the message dict to send | ||
client_msg_validated = ClientMessage.model_validate({"params": params}) | ||
msg = client_msg_validated.convert_to_server() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we always need to add id
to the output of convert_to_server
. Let's add it as a parameter for the convert_to_server
function. Therefore, we eliminate the chance of forgetting to add it.
await self.ws.send(json.dumps(msg)) | ||
|
||
# await the response for the sent ws message from the server | ||
msg = await future |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's add some timeout there too. we don't want to wait indefinitely if there is a problem
future.set_result(msg_json) | ||
|
||
async def get_opportunities( | ||
self, chain_id: str | None = None, timeout_secs: int = 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's use self.httpx_options
instead of this timeout parameter
return opportunities | ||
|
||
async def submit_opportunity( | ||
self, opportunity: OpportunityParams, timeout_secs: int = 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's use self.httpx_options
instead of this timeout parameter
""" | ||
Returns the websocket handler loop. | ||
""" | ||
await self.start_ws() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this function needs to start the websocket on its own. Maybe if we haven't started the ws yet, it's better to return None
here
resp.raise_for_status() | ||
|
||
opportunities = [ | ||
Opportunity.process_opportunity_dict(opportunity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this list should not include None values
on: | ||
push: | ||
tags: | ||
- "v*" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's make this a bit more specific. maybe python-v*
@@ -0,0 +1,27 @@ | |||
[tool.poetry] | |||
name = "express-relay-utils" | |||
version = "0.0.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's make this 0.2.0 to match the server version
express_relay/sdk/python/mypy.ini
Outdated
check_untyped_defs = True | ||
no_implicit_reexport = True | ||
|
||
; # for strict mypy: (this is the tricky one :-)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we will enable this anytime soon let's just remove it
@@ -0,0 +1,27 @@ | |||
[tool.poetry] | |||
name = "express-relay-utils" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's remove utils
from here and the folder structure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix this and the previous comments remaining before merge
"Cannot handle opportunity version: {%s}. Please upgrade your client." | ||
) | ||
|
||
@model_validator(mode="after") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should do this before the validation. If the new schema doesn't have some of these fields we will receive another exception which we will not handle
""" | ||
try: | ||
return cls.model_validate(opportunity_dict) | ||
except Exception as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of catching a general exception we can throw something custom like UnsupportedOpportunity() and just catch that here.