diff --git a/express_relay/sdk/python/express_relay_utils/express_relay_client.py b/express_relay/sdk/python/express_relay_utils/express_relay_client.py index 046c4bcf9c..95c08a4a82 100644 --- a/express_relay/sdk/python/express_relay_utils/express_relay_client.py +++ b/express_relay/sdk/python/express_relay_utils/express_relay_client.py @@ -2,6 +2,7 @@ import json import urllib.parse from typing import Callable +from uuid import UUID import httpx import web3 @@ -18,7 +19,7 @@ class BidInfo: - def __init__(self, opportunity_id: str, opportunity_bid: OpportunityBid): + def __init__(self, opportunity_id: UUID, opportunity_bid: OpportunityBid): self.opportunity_id = opportunity_id self.opportunity_bid = opportunity_bid @@ -56,6 +57,7 @@ async def start_ws( Initializes the websocket connection to the server, if not already connected. Args: + opportunity_callback: An async function that serves as the callback on a new liquidation opportunity. Should take in one external argument of type OpportunityParamsWithMetadata. kwargs: Keyword arguments to pass to the websocket connection. Returns: The websocket task. @@ -126,14 +128,14 @@ async def send_ws_message(self, msg: dict): self.ws_msg_counter += 1 future = asyncio.get_event_loop().create_future() + self.ws_msg_futures[msg["id"]] = future await self.ws.send(json.dumps(msg)) - self.ws_msg_futures[msg["id"]] = future - # await the response msg for the subscription from the server + # await the response for the subscription from the server msg = await future - self.process_msg(msg) + self.process_response_msg(msg) async def subscribe_chains(self, chain_ids: list[str]): """ @@ -165,16 +167,16 @@ async def unsubscribe_chains(self, chain_ids: list[str]): } await self.send_ws_message(json_unsubscribe) - def process_msg(self, msg: dict): + def process_response_msg(self, msg: dict): """ - Processes a message received from the server via websocket. + Processes a response message received from the server via websocket. Args: msg: The message to process. """ if msg.get("status") and msg.get("status") != "success": raise ExpressRelayClientException( - f"Error in websocket subscription: {msg.get('result')}" + f"Error in websocket with message id {msg.get('id')}: {msg.get('result')}" ) async def ws_handler( @@ -197,19 +199,18 @@ async def ws_handler( if msg.get("id"): future = self.ws_msg_futures.pop(msg["id"]) future.set_result(msg) - else: - self.process_msg(msg) if msg.get("type") == "new_opportunity": - opportunity = msg["opportunity"] - opportunity = OpportunityParamsWithMetadata.from_dict(opportunity) + opportunity = OpportunityParamsWithMetadata.from_dict( + msg["opportunity"] + ) if opportunity_callback is not None: asyncio.create_task(opportunity_callback(opportunity)) async def submit_opportunity( self, opportunity: OpportunityParams, timeout: int = 10 - ) -> str: + ) -> UUID: """ Submits an opportunity to the liquidation server. @@ -228,9 +229,9 @@ async def submit_opportunity( timeout=timeout, ) resp.raise_for_status() - return resp.json()["opportunity_id"] + return UUID(resp.json()["opportunity_id"]) - async def submit_bid(self, bid_info: BidInfo, timeout: int = 10) -> str: + async def submit_bid(self, bid_info: BidInfo, timeout: int = 10) -> UUID: """ Submits a bid to the liquidation server. @@ -244,14 +245,14 @@ async def submit_bid(self, bid_info: BidInfo, timeout: int = 10) -> str: resp = await client.post( urllib.parse.urlparse(self.server_url) ._replace( - path=f"/v1/liquidation/opportunities/{bid_info.opportunity_id}/bids" + path=f"/v1/liquidation/opportunities/{str(bid_info.opportunity_id)}/bids" ) .geturl(), json=bid_info.opportunity_bid.to_dict(), timeout=timeout, ) resp.raise_for_status() - return resp.json()["id"] + return UUID(resp.json()["id"]) def sign_bid( @@ -314,7 +315,7 @@ def sign_bid( opportunity_bid = OpportunityBid.from_dict(opportunity_bid) bid_info = BidInfo( - opportunity_id=liquidation_opportunity.opportunity_id, + opportunity_id=UUID(liquidation_opportunity.opportunity_id), opportunity_bid=opportunity_bid, ) diff --git a/express_relay/sdk/python/express_relay_utils/searcher/examples/simple_searcher.py b/express_relay/sdk/python/express_relay_utils/searcher/examples/simple_searcher.py index c33f426e34..a7bc8c9f9a 100644 --- a/express_relay/sdk/python/express_relay_utils/searcher/examples/simple_searcher.py +++ b/express_relay/sdk/python/express_relay_utils/searcher/examples/simple_searcher.py @@ -10,16 +10,16 @@ logger = logging.getLogger(__name__) +NAIVE_BID = 10 # Set validity (naively) to max uint256 VALID_UNTIL_MAX = 2**256 - 1 class SimpleSearcher: - def __init__(self, server_url: str, private_key: str, default_bid: int): + def __init__(self, server_url: str, private_key: str): self.client = ExpressRelayClient(server_url) self.private_key = private_key self.liquidator = Account.from_key(private_key).address - self.default_bid = default_bid def assess_liquidation_opportunity( self, @@ -31,13 +31,13 @@ def assess_liquidation_opportunity( This function determines whether the given opportunity deals with the specified repay and receipt tokens that the searcher wishes to transact in and whether it is profitable to execute the liquidation. There are many ways to evaluate this, but the most common way is to check that the value of the amount the searcher will receive from the liquidation exceeds the value of the amount repaid. Individual searchers will have their own methods to determine market impact and the profitability of conducting a liquidation. This function can be expanded to include external prices to perform this evaluation. - In this simple searcher, the function always (naively) returns a BidInfo object with the default bid and a valid_until timestamp. + In this simple searcher, the function always (naively) returns a BidInfo object with a default bid and valid_until timestamp. Args: opp: A OpportunityParamsWithMetadata object, representing a single liquidation opportunity. Returns: If the opportunity is deemed worthwhile, this function can return a BidInfo object, whose contents can be submitted to the auction server. If the opportunity is not deemed worthwhile, this function can return None. """ - bid_info = sign_bid(opp, self.default_bid, VALID_UNTIL_MAX, self.private_key) + bid_info = sign_bid(opp, NAIVE_BID, VALID_UNTIL_MAX, self.private_key) return bid_info @@ -53,11 +53,11 @@ async def opportunity_callback(self, opp: OpportunityParamsWithMetadata): try: await self.client.submit_bid(bid_info) logger.info( - f"Submitted bid amount {bid_info.opportunity_bid.amount} for opportunity {bid_info.opportunity_id}" + f"Submitted bid amount {bid_info.opportunity_bid.amount} for opportunity {str(bid_info.opportunity_id)}" ) except Exception as e: logger.error( - f"Error submitting bid amount {bid_info.opportunity_bid.amount} for opportunity {bid_info.opportunity_id}: {e}" + f"Error submitting bid amount {bid_info.opportunity_bid.amount} for opportunity {str(bid_info.opportunity_id)}: {e}" ) @@ -77,12 +77,6 @@ async def main(): nargs="+", help="Chain ID(s) of the network(s) to monitor for liquidation opportunities", ) - parser.add_argument( - "--bid", - type=int, - default=10, - help="Default amount of bid for liquidation opportunities", - ) parser.add_argument( "--server-url", type=str, @@ -102,7 +96,7 @@ async def main(): sk_liquidator = args.private_key - simple_searcher = SimpleSearcher(args.server_url, sk_liquidator, args.bid) + simple_searcher = SimpleSearcher(args.server_url, sk_liquidator) logger.info("Liquidator address: %s", simple_searcher.liquidator) task = await simple_searcher.client.start_ws(simple_searcher.opportunity_callback)