diff --git a/src/polymarket/__init__.py b/src/polymarket/__init__.py index d35baef..75b19cb 100644 --- a/src/polymarket/__init__.py +++ b/src/polymarket/__init__.py @@ -152,6 +152,7 @@ RfqRequestorPublicId, RfqSession, RfqSide, + RfqTradeEvent, ) from polymarket.transactions import ( EoaTransactionHandle, @@ -290,6 +291,7 @@ "RfqRequestorPublicId", "RfqSession", "RfqSide", + "RfqTradeEvent", "SearchResults", "SearchTag", "SecureClient", diff --git a/src/polymarket/_internal/rfq.py b/src/polymarket/_internal/rfq.py index 21382f3..23b1301 100644 --- a/src/polymarket/_internal/rfq.py +++ b/src/polymarket/_internal/rfq.py @@ -59,6 +59,7 @@ RfqRequestedSize, RfqRequestedSizeUnit, RfqSide, + RfqTradeEvent, ) from polymarket.types import EvmAddress, HexString, TransactionHash @@ -434,6 +435,8 @@ def _on_message(self, raw: object) -> None: tx_hash=TransactionHash(tx_hash) if isinstance(tx_hash, str) else None, ) ) + elif message_type == "RFQ_TRADE": + self._push(_parse_trade(message)) elif message_type == "RFQ_ERROR": self._handle_rfq_error(message) except BaseException as error: @@ -634,6 +637,23 @@ def _parse_confirmation_request( ) +def _parse_trade(raw: dict[str, object]) -> RfqTradeEvent: + return RfqTradeEvent( + type="trade", + rfq_id=_expect_str(raw, "rfq_id"), + requester_id=_expect_str(raw, "requester_id"), + condition_id=_expect_combo_condition_id(raw, "condition_id"), + leg_position_ids=tuple( + PositionId(item) for item in _expect_str_list(raw, "leg_position_ids") + ), + direction=RfqDirection(_expect_str(raw, "direction")), + side=RfqSide(_expect_str(raw, "side")), + price=_e6_to_decimal(_expect_str(raw, "price_e6")), + size=_e6_to_decimal(_expect_str(raw, "size_e6")), + executed_at=_expect_int(raw, "executed_at"), + ) + + def _parse_requested_size(raw: dict[str, object]) -> RfqRequestedSize: return RfqRequestedSize( unit=RfqRequestedSizeUnit(_expect_str(raw, "unit")), diff --git a/src/polymarket/rfq.py b/src/polymarket/rfq.py index d94118c..053b283 100644 --- a/src/polymarket/rfq.py +++ b/src/polymarket/rfq.py @@ -111,6 +111,20 @@ class RfqExecutionUpdateEvent: tx_hash: TransactionHash | None = None +@dataclass(frozen=True, slots=True, kw_only=True) +class RfqTradeEvent: + type: Literal["trade"] + rfq_id: RfqId + requester_id: RfqRequestorPublicId + condition_id: ComboConditionId + leg_position_ids: tuple[PositionId, ...] + direction: RfqDirection + side: RfqSide + price: Decimal + size: Decimal + executed_at: int + + @dataclass(frozen=True, slots=True, kw_only=True) class RfqQuoteRequestEvent: type: Literal["quote_request"] @@ -166,7 +180,9 @@ async def decline(self) -> RfqConfirmationAck: ) -RfqEvent = RfqQuoteRequestEvent | RfqConfirmationRequestEvent | RfqExecutionUpdateEvent +RfqEvent = ( + RfqQuoteRequestEvent | RfqConfirmationRequestEvent | RfqExecutionUpdateEvent | RfqTradeEvent +) class RfqQuoteRejectedError(PolymarketError): @@ -259,4 +275,5 @@ async def __aexit__( "RfqRequestorPublicId", "RfqSession", "RfqSide", + "RfqTradeEvent", ] diff --git a/tests/integration/test_rfq.py b/tests/integration/test_rfq.py index 02af98d..c48a181 100644 --- a/tests/integration/test_rfq.py +++ b/tests/integration/test_rfq.py @@ -16,10 +16,12 @@ RfqConfirmationRequestEvent, RfqErrorCode, RfqExecutionStatus, + RfqExecutionUpdateEvent, RfqQuoteRejectedError, RfqQuoteRequestEvent, RfqQuoteSource, RfqRequestedSizeUnit, + RfqTradeEvent, ) pytestmark = pytest.mark.anyio @@ -118,6 +120,7 @@ async def handler(ws: ServerConnection) -> None: } ) ) + await ws.send(json.dumps(_trade_message())) async with ( _ws_server(handler) as ws_url, @@ -135,7 +138,7 @@ async def handler(ws: ServerConnection) -> None: ack = await event.confirm() assert ack.rfq_id == RFQ_ID assert ack.quote_id == QUOTE_ID - else: + elif isinstance(event, RfqExecutionUpdateEvent): assert event.status is RfqExecutionStatus.MINED assert event.tx_hash == TX_HASH break @@ -166,6 +169,39 @@ async def handler(ws: ServerConnection) -> None: } +@pytest.mark.integration +async def test_rfq_session_receives_confirmed_trade_broadcast( + require_env: Callable[[str], str], +) -> None: + async def handler(ws: ServerConnection) -> None: + async for raw in ws: + assert isinstance(raw, str) + frame = json.loads(raw) + if frame["type"] == "auth": + await ws.send(json.dumps({"type": "auth", "success": True})) + await ws.send(json.dumps(_trade_message())) + + async with ( + _ws_server(handler) as ws_url, + _rfq_client(require_env, ws_url) as client, + client.open_rfq_session() as session, + ): + async for event in session: + assert isinstance(event, RfqTradeEvent) + assert event.type == "trade" + assert event.rfq_id == RFQ_ID + assert event.requester_id == "requester-abc" + assert event.condition_id == CONDITION_ID + assert event.leg_position_ids == (YES_POSITION_ID, NO_POSITION_ID) + assert event.direction == "BUY" + assert event.side == "YES" + assert event.price == Decimal("0.125") + assert event.size == Decimal("0.8") + assert not hasattr(event, "tx_hash") + assert event.executed_at == 1780854786039 + break + + @pytest.mark.integration async def test_rfq_session_quotes_explicit_inventory_size( require_env: Callable[[str], str], @@ -817,6 +853,21 @@ def _manual_quote_frame() -> dict[str, Any]: } +def _trade_message() -> dict[str, object]: + return { + "type": "RFQ_TRADE", + "rfq_id": RFQ_ID, + "requester_id": "requester-abc", + "condition_id": CONDITION_ID, + "leg_position_ids": [YES_POSITION_ID, NO_POSITION_ID], + "direction": "BUY", + "side": "YES", + "price_e6": "125000", + "size_e6": "800000", + "executed_at": 1780854786039, + } + + def _first_frame(frames: list[dict[str, Any]], frame_type: str) -> dict[str, Any]: for frame in frames: if frame.get("type") == frame_type: