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

Express relay python searcher #1319

Merged
merged 59 commits into from
Mar 15, 2024
Merged

Express relay python searcher #1319

merged 59 commits into from
Mar 15, 2024

Conversation

anihamde
Copy link
Contributor

  • Reorg Js searcher sdk
  • Drop python searcher sdk (w/o websocket; to be added)

Copy link

vercel bot commented Feb 22, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Ignored Deployments
Name Status Preview Comments Updated (UTC)
example-oracle-amm ⬜️ Ignored (Inspect) Visit Preview Mar 15, 2024 7:03pm
xc-admin-frontend ⬜️ Ignored (Inspect) Visit Preview Mar 15, 2024 7:03pm

express_relay/sdk/python/searcher/searcher_utils.py Outdated Show resolved Hide resolved
express_relay/sdk/python/searcher/searcher_utils.py Outdated Show resolved Hide resolved
express_relay/sdk/python/searcher/pyproject.toml Outdated Show resolved Hide resolved
self.ws_msg_counter = 0

async def get_liquidation_opportunities(self) -> list[OpportunityParamsWithMetadata]:
async with httpx.AsyncClient() as client:
Copy link
Collaborator

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

Copy link
Contributor Author

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/searcher/searcher_utils.py Outdated Show resolved Hide resolved
express_relay/sdk/python/searcher/searcher_utils.py Outdated Show resolved Hide resolved
express_relay/sdk/python/searcher/searcher_utils.py Outdated Show resolved Hide resolved
express_relay/sdk/python/searcher/README.md Outdated Show resolved Hide resolved
msg["id"] = str(self.ws_msg_counter)
self.ws_msg_counter += 1

await self.ws.send(json.dumps(msg))
Copy link
Collaborator

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

@@ -4,6 +4,7 @@ repos:
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
exclude: openapi_client
Copy link
Collaborator

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

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

Choose a reason for hiding this comment

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

what does this mean?

$ poetry add express-relay-utils
```

### openapi-generator
Copy link
Collaborator

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):
Copy link
Collaborator

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:
Copy link
Collaborator

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):
Copy link
Collaborator

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):
Copy link
Collaborator

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:
Copy link
Collaborator

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

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

Copy link
Contributor Author

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.
Copy link
Collaborator

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:
Copy link
Collaborator

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.

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 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

Copy link
Collaborator

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"
Copy link
Collaborator

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?

Copy link
Contributor Author

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/

@@ -0,0 +1,25 @@
# Pyth Express Relay Python SDK
Copy link
Collaborator

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."
Copy link
Collaborator

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:
Copy link
Collaborator

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

Copy link
Contributor Author

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()
Copy link
Collaborator

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] = {},
Copy link
Collaborator

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()
Copy link
Collaborator

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

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

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

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()
Copy link
Collaborator

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)
Copy link
Collaborator

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*"
Copy link
Collaborator

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"
Copy link
Collaborator

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

check_untyped_defs = True
no_implicit_reexport = True

; # for strict mypy: (this is the tricky one :-))
Copy link
Collaborator

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"
Copy link
Collaborator

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

Copy link
Collaborator

@m30m m30m left a 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")
Copy link
Collaborator

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:
Copy link
Collaborator

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.

@anihamde anihamde merged commit 9256673 into main Mar 15, 2024
5 checks passed
@anihamde anihamde deleted the express-relay-python-searcher branch March 15, 2024 19:08
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