Skip to content
This repository was archived by the owner on May 25, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions py_clob_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,12 @@ def create_and_post_order(

def cancel(self, order_id):
"""
Cancels an order
Cancels an order and returns the full order object (including sizeMatched).

If the order was successfully canceled, fetches and returns the order details
so callers can inspect ``sizeMatched`` without waiting for a fill event.
If the cancellation failed, returns the raw API response unchanged.

Level 2 Auth required
"""
self.assert_level_2_auth()
Expand All @@ -675,15 +680,28 @@ def cancel(self, order_id):
serialized_body=json.dumps(body, separators=(",", ":"), ensure_ascii=False),
)
headers = create_level_2_headers(self.signer, self.creds, request_args)
return delete(
response = delete(
"{}{}".format(self.host, CANCEL),
headers=headers,
data=request_args.serialized_body,
)
canceled = response.get("canceled", [])
if order_id in canceled:
try:
return self.get_order(order_id)
except Exception:
return response
return response

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

cancel() returns incompatible dict shapes across code paths

Medium Severity

cancel() returns two structurally incompatible dict types: on success it returns a full order object (e.g. keys id, status, size_matched), but on failure or get_order error it returns the raw API response (keys canceled, not_canceled). Callers who previously inspected result.get("canceled") to determine outcome will silently get None on a successful cancel, incorrectly concluding the cancel failed. In contrast, cancel_orders() always returns a consistent {"canceled": [...], "not_canceled": {...}} structure. The asymmetry between cancel() and cancel_orders() makes uniform handling impossible without brittle key-sniffing.

Fix in Cursor Fix in Web


def cancel_orders(self, order_ids):
"""
Cancels orders
Cancels a list of orders and returns full order objects (including sizeMatched)
for each successfully canceled order.

Returns a dict with:
- ``canceled``: list of full order objects for successfully canceled orders
- ``not_canceled``: dict mapping order IDs to error reasons (unchanged from API)

Level 2 Auth required
"""
self.assert_level_2_auth()
Expand All @@ -696,9 +714,20 @@ def cancel_orders(self, order_ids):
serialized_body=serialized,
)
headers = create_level_2_headers(self.signer, self.creds, request_args)
return delete(
response = delete(
"{}{}".format(self.host, CANCEL_ORDERS), headers=headers, data=serialized
)
canceled_ids = response.get("canceled", [])
canceled_orders = []
for oid in canceled_ids:
try:
canceled_orders.append(self.get_order(oid))
except Exception:
canceled_orders.append({"id": oid})
return {
"canceled": canceled_orders,
"not_canceled": response.get("not_canceled", {}),
}

def cancel_all(self):
"""
Expand Down
146 changes: 146 additions & 0 deletions tests/test_cancel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch


ORDER_ID = "0xabc123"
FULL_ORDER = {
"id": ORDER_ID,
"status": "CANCELED",
"side": "BUY",
"original_size": "100000000",
"size_matched": "30000000",
"price": "0.5",
"asset_id": "52114319501245915516055106046884209969926127482827954674443846427813813222426",
}


def _make_client():
"""Return a ClobClient with auth mocked out."""
from py_clob_client.client import ClobClient

client = ClobClient.__new__(ClobClient)
client.host = "https://clob.polymarket.com"
client.signer = MagicMock()
client.creds = MagicMock()
client.assert_level_2_auth = MagicMock()
return client


class TestCancel(TestCase):
@patch("py_clob_client.client.create_level_2_headers", return_value={})
@patch("py_clob_client.client.delete")
def test_cancel_returns_full_order_on_success(self, mock_delete, _mock_headers):
mock_delete.return_value = {"canceled": [ORDER_ID], "not_canceled": {}}
client = _make_client()
client.get_order = MagicMock(return_value=FULL_ORDER)

result = client.cancel(ORDER_ID)

client.get_order.assert_called_once_with(ORDER_ID)
self.assertEqual(result, FULL_ORDER)
self.assertIn("size_matched", result)

@patch("py_clob_client.client.create_level_2_headers", return_value={})
@patch("py_clob_client.client.delete")
def test_cancel_returns_raw_response_when_not_canceled(self, mock_delete, _mock_headers):
raw = {"canceled": [], "not_canceled": {ORDER_ID: "Order not found"}}
mock_delete.return_value = raw
client = _make_client()
client.get_order = MagicMock()

result = client.cancel(ORDER_ID)

client.get_order.assert_not_called()
self.assertEqual(result, raw)

@patch("py_clob_client.client.create_level_2_headers", return_value={})
@patch("py_clob_client.client.delete")
def test_cancel_returns_raw_response_when_get_order_fails(self, mock_delete, _mock_headers):
raw = {"canceled": [ORDER_ID], "not_canceled": {}}
mock_delete.return_value = raw
client = _make_client()
client.get_order = MagicMock(side_effect=Exception("network error"))

result = client.cancel(ORDER_ID)

self.assertEqual(result, raw)


class TestCancelOrders(TestCase):
ORDER_ID_2 = "0xdef456"

@patch("py_clob_client.client.create_level_2_headers", return_value={})
@patch("py_clob_client.client.delete")
def test_cancel_orders_returns_full_order_objects(self, mock_delete, _mock_headers):
mock_delete.return_value = {
"canceled": [ORDER_ID],
"not_canceled": {},
}
client = _make_client()
client.get_order = MagicMock(return_value=FULL_ORDER)

result = client.cancel_orders([ORDER_ID])

self.assertIn("canceled", result)
self.assertIn("not_canceled", result)
self.assertEqual(len(result["canceled"]), 1)
self.assertEqual(result["canceled"][0], FULL_ORDER)
self.assertIn("size_matched", result["canceled"][0])

@patch("py_clob_client.client.create_level_2_headers", return_value={})
@patch("py_clob_client.client.delete")
def test_cancel_orders_preserves_not_canceled(self, mock_delete, _mock_headers):
mock_delete.return_value = {
"canceled": [ORDER_ID],
"not_canceled": {self.ORDER_ID_2: "Order already matched"},
}
client = _make_client()
client.get_order = MagicMock(return_value=FULL_ORDER)

result = client.cancel_orders([ORDER_ID, self.ORDER_ID_2])

self.assertEqual(result["not_canceled"], {self.ORDER_ID_2: "Order already matched"})

@patch("py_clob_client.client.create_level_2_headers", return_value={})
@patch("py_clob_client.client.delete")
def test_cancel_orders_falls_back_to_id_dict_when_get_order_fails(self, mock_delete, _mock_headers):
mock_delete.return_value = {"canceled": [ORDER_ID], "not_canceled": {}}
client = _make_client()
client.get_order = MagicMock(side_effect=Exception("network error"))

result = client.cancel_orders([ORDER_ID])

self.assertEqual(result["canceled"], [{"id": ORDER_ID}])

@patch("py_clob_client.client.create_level_2_headers", return_value={})
@patch("py_clob_client.client.delete")
def test_cancel_orders_handles_empty_response(self, mock_delete, _mock_headers):
mock_delete.return_value = {"canceled": [], "not_canceled": {}}
client = _make_client()
client.get_order = MagicMock()

result = client.cancel_orders([])

client.get_order.assert_not_called()
self.assertEqual(result["canceled"], [])
self.assertEqual(result["not_canceled"], {})

@patch("py_clob_client.client.create_level_2_headers", return_value={})
@patch("py_clob_client.client.delete")
def test_cancel_orders_continues_after_single_get_order_failure(self, mock_delete, _mock_headers):
ORDER_ID_3 = "0xghi789"
FULL_ORDER_3 = {**FULL_ORDER, "id": ORDER_ID_3}
mock_delete.return_value = {
"canceled": [ORDER_ID, ORDER_ID_3],
"not_canceled": {},
}
client = _make_client()
client.get_order = MagicMock(
side_effect=[Exception("transient error"), FULL_ORDER_3]
)

result = client.cancel_orders([ORDER_ID, ORDER_ID_3])

self.assertEqual(len(result["canceled"]), 2)
self.assertEqual(result["canceled"][0], {"id": ORDER_ID})
self.assertEqual(result["canceled"][1], FULL_ORDER_3)