Skip to content
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
39 changes: 33 additions & 6 deletions py_clob_client_v2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,27 @@ def get_clob_market_info(self, condition_id: str) -> dict:

return result

def warm_up_order_metadata(
self,
condition_id: str,
builder_code: Optional[str] = None,
) -> None:
"""Cache the public metadata needed to construct orders for a market."""
result = self.get_clob_market_info(condition_id)
version = self.__resolve_version()
token_ids = [token["t"] for token in result["t"] if token and token.get("t")]

if version == 1:
for token_id in token_ids:
self.get_fee_rate_bps(token_id)

if (
builder_code
and builder_code != BYTES32_ZERO
and builder_code not in self.__builder_fee_rates
):
self.__load_builder_fee_rate(builder_code)
Comment thread
casterkay marked this conversation as resolved.

def get_order_book(self, token_id: str):
return self._get(
f"{self.host}{GET_ORDER_BOOK}", params={"token_id": token_id}
Expand Down Expand Up @@ -1206,13 +1227,19 @@ def __ensure_builder_fee_rate_cached(self, builder_code: str):
if builder_code in self.__builder_fee_rates:
return
try:
result = self._get(f"{self.host}{GET_BUILDER_FEE_RATE}{builder_code}")
self.__builder_fee_rates[builder_code] = BuilderFeeRate(
maker=result.get("builder_maker_fee_rate_bps", 0) / BUILDER_FEES_BPS,
taker=result.get("builder_taker_fee_rate_bps", 0) / BUILDER_FEES_BPS,
)
self.__load_builder_fee_rate(builder_code)
except Exception:
logging.warning("failed to fetch builder fee rate for %s, will retry on next order", builder_code)
logging.warning(
"failed to fetch builder fee rate for %s, will retry on next order",
builder_code,
)

def __load_builder_fee_rate(self, builder_code: str) -> None:
result = self._get(f"{self.host}{GET_BUILDER_FEE_RATE}{builder_code}")
self.__builder_fee_rates[builder_code] = BuilderFeeRate(
maker=result.get("builder_maker_fee_rate_bps", 0) / BUILDER_FEES_BPS,
taker=result.get("builder_taker_fee_rate_bps", 0) / BUILDER_FEES_BPS,
)

def __ensure_market_info_cached(self, token_id: str):
if token_id in self.__fee_infos:
Expand Down
19 changes: 16 additions & 3 deletions py_clob_client_v2/order_utils/exchange_order_builder_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,14 @@ def __init__(
def build_signed_order(self, order_data: OrderDataV1) -> SignedOrderV1:
order = self.build_order(order_data)
typed_data = self.build_order_typed_data(order)
signature = self.build_order_signature(typed_data)
return SignedOrderV1(**{**dataclasses.asdict(order), "signature": signature})
signature, order_hash = self._build_order_signature_and_hash(typed_data)
return SignedOrderV1(
**{
**dataclasses.asdict(order),
"signature": signature,
"order_hash": order_hash,
}
)

def build_order(self, order_data: OrderDataV1) -> OrderV1:
signer_addr = order_data.signer if order_data.signer else order_data.maker
Expand Down Expand Up @@ -94,9 +100,16 @@ def build_order_typed_data(self, order: OrderV1) -> dict:
}

def build_order_signature(self, typed_data: dict) -> str:
signature, _ = self._build_order_signature_and_hash(typed_data)
return signature

def _build_order_signature_and_hash(self, typed_data: dict) -> tuple[str, str]:
encoded = encode_typed_data(full_message=typed_data)
signed = Account.sign_message(encoded, private_key=self.signer.private_key)
return "0x" + signed.signature.hex()
return (
"0x" + signed.signature.hex(),
"0x" + signed.message_hash.hex(),
)

def build_order_hash(self, typed_data: dict) -> str:
encoded = encode_typed_data(full_message=typed_data)
Expand Down
20 changes: 18 additions & 2 deletions py_clob_client_v2/order_utils/exchange_order_builder_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,14 @@ def __init__(
def build_signed_order(self, order_data: OrderDataV2) -> SignedOrderV2:
order = self.build_order(order_data)
typed_data = self.build_order_typed_data(order)
signature = self.build_order_signature(typed_data)
return SignedOrderV2(**{**dataclasses.asdict(order), "signature": signature})
signature, order_hash = self._build_order_signature_and_hash(typed_data)
return SignedOrderV2(
**{
**dataclasses.asdict(order),
"signature": signature,
"order_hash": order_hash,
}
)

def build_order(self, order_data: OrderDataV2) -> OrderV2:
signer_addr = order_data.signer if order_data.signer else order_data.maker
Expand Down Expand Up @@ -158,6 +164,16 @@ def build_order_signature(self, typed_data: dict) -> str:
signed = Account.sign_message(encoded, private_key=self.signer.private_key)
return "0x" + signed.signature.hex()

def _build_order_signature_and_hash(self, typed_data: dict) -> tuple[str, str]:
encoded = encode_typed_data(full_message=typed_data)

if typed_data["message"]["signatureType"] == int(SignatureTypeV2.POLY_1271):
order_hash = "0x" + _hash_message(encoded).hex()
return self._build_poly_1271_order_signature(typed_data), order_hash

signed = Account.sign_message(encoded, private_key=self.signer.private_key)
return "0x" + signed.signature.hex(), "0x" + signed.message_hash.hex()

def _build_poly_1271_order_signature(self, typed_data: dict) -> str:
message = typed_data["message"]
contents_hash = _keccak(
Expand Down
1 change: 1 addition & 0 deletions py_clob_client_v2/order_utils/model/order_data_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class SignedOrderV1(OrderV1):
"""A signed V1 order including the EIP712 signature."""

signature: str = ""
order_hash: str = ""


def order_to_json_v1(
Expand Down
1 change: 1 addition & 0 deletions py_clob_client_v2/order_utils/model/order_data_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class SignedOrderV2(OrderV2):
"""A signed V2 order including the EIP712 signature."""

signature: str = ""
order_hash: str = ""


def order_to_json_v2(
Expand Down
40 changes: 38 additions & 2 deletions tests/order_utils/test_exchange_order_builder_v1.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from unittest import TestCase
from unittest.mock import patch

from py_clob_client_v2.constants import AMOY, ZERO_ADDRESS
from py_clob_client_v2.config import get_contract_config
from py_clob_client_v2.order_utils.exchange_order_builder_v1 import ExchangeOrderBuilderV1
from py_clob_client_v2.order_utils.model.order_data_v1 import OrderDataV1
from py_clob_client_v2.order_utils.exchange_order_builder_v1 import (
ExchangeOrderBuilderV1,
)
from py_clob_client_v2.order_utils.model.order_data_v1 import (
OrderDataV1,
order_to_json_v1,
)
from py_clob_client_v2.order_utils.model.ctf_exchange_v1_typed_data import (
CTF_EXCHANGE_V1_DOMAIN_NAME,
CTF_EXCHANGE_V1_DOMAIN_VERSION,
Expand Down Expand Up @@ -36,6 +42,7 @@
signatureType=SignatureTypeV1.EOA,
)


class TestExchangeOrderBuilderV1CTF(TestCase):
"""Tests against the CTF Exchange (Polymarket CTF Exchange v1)."""

Expand Down Expand Up @@ -206,6 +213,35 @@ def test_build_signed_order_specific_salt(self):
signed.signature,
"0x302cd9abd0b5fcaa202a344437ec0b6660da984e24ae9ad915a592a90facf5a51bb8a873cd8d270f070217fea1986531d5eec66f1162a81f66e026db653bf7ce1c",
)
self.assertEqual(
signed.order_hash,
"0x02ca1d1aa31103804173ad1acd70066cb6c1258a4be6dada055111f9a7ea4e55",
)
self.assertNotIn(
"order_hash", order_to_json_v1(signed, "owner", "GTC")["order"]
)

def test_build_signed_order_encodes_typed_data_once(self):
from py_clob_client_v2.order_utils import exchange_order_builder_v1 as module

with (
patch.object(
module,
"encode_typed_data",
wraps=module.encode_typed_data,
) as encode,
patch.object(
module,
"_hash_message",
wraps=module._hash_message,
) as duplicate_hash,
):
signed = self.builder.build_signed_order(_ORDER_DATA)

self.assertTrue(signed.order_hash.startswith("0x"))
encode.assert_called_once()
duplicate_hash.assert_not_called()


class TestExchangeOrderBuilderV1NegRisk(TestCase):
"""Tests against the Neg Risk CTF Exchange."""
Expand Down
85 changes: 84 additions & 1 deletion tests/order_utils/test_exchange_order_builder_v2.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from unittest import TestCase
from unittest.mock import patch

from py_clob_client_v2.config import get_contract_config
from py_clob_client_v2.constants import AMOY, BYTES32_ZERO
from py_clob_client_v2.order_utils.exchange_order_builder_v2 import (
ORDER_TYPE_STRING,
ExchangeOrderBuilderV2,
)
from py_clob_client_v2.order_utils.model.order_data_v2 import OrderDataV2
from py_clob_client_v2.order_utils.model.order_data_v2 import (
OrderDataV2,
order_to_json_v2,
)
from py_clob_client_v2.order_utils.model.side import Side
from py_clob_client_v2.order_utils.model.signature_type_v2 import SignatureTypeV2
from py_clob_client_v2.signer import Signer
Expand Down Expand Up @@ -50,6 +54,24 @@ def _poly_1271_order_data() -> OrderDataV2:
)


def _eoa_signed_order_data(signature_type: SignatureTypeV2) -> OrderDataV2:
maker = (
SIGNER.address() if signature_type == SignatureTypeV2.EOA else DEPOSIT_WALLET
)
return OrderDataV2(
maker=maker,
signer=SIGNER.address(),
tokenId="1234",
makerAmount="100000000",
takerAmount="50000000",
side=Side.BUY,
signatureType=signature_type,
timestamp=FIXED_TIMESTAMP,
metadata=BYTES32_ZERO,
builder=BYTES32_ZERO,
)


class TestExchangeOrderBuilderV2CTF(TestCase):
def setUp(self):
self.builder = ExchangeOrderBuilderV2(
Expand Down Expand Up @@ -127,3 +149,64 @@ def test_build_signed_order_poly_1271_matches_expected_signature(self):
self.assertEqual(signed.signer, DEPOSIT_WALLET)
self.assertEqual(signed.signatureType, SignatureTypeV2.POLY_1271)
self.assertEqual(signed.signature, EXPECTED_POLY_1271_SIGNATURE)
self.assertEqual(
signed.order_hash,
"0x2afca94626db91b2d556c351584a4cd93e13da32e377dc48c7983e43afa5ab47",
)
self.assertNotIn(
"order_hash", order_to_json_v2(signed, "owner", "GTC")["order"]
)

def test_build_signed_order_poly_1271_encodes_typed_data_once(self):
from py_clob_client_v2.order_utils import exchange_order_builder_v2 as module

with (
patch.object(
module,
"encode_typed_data",
wraps=module.encode_typed_data,
) as encode,
patch.object(
module,
"_hash_message",
wraps=module._hash_message,
) as order_hash,
):
signed = self.builder.build_signed_order(_poly_1271_order_data())

self.assertTrue(signed.order_hash.startswith("0x"))
encode.assert_called_once()
order_hash.assert_called_once()

def test_build_signed_eoa_family_orders_carry_hash_from_one_encode(self):
from py_clob_client_v2.order_utils import exchange_order_builder_v2 as module

for signature_type in (
SignatureTypeV2.EOA,
SignatureTypeV2.POLY_PROXY,
SignatureTypeV2.POLY_GNOSIS_SAFE,
):
with self.subTest(signature_type=signature_type):
with (
patch.object(
module,
"encode_typed_data",
wraps=module.encode_typed_data,
) as encode,
patch.object(
module,
"_hash_message",
wraps=module._hash_message,
) as duplicate_hash,
):
signed = self.builder.build_signed_order(
_eoa_signed_order_data(signature_type)
)

typed_data = self.builder.build_order_typed_data(signed)
self.assertEqual(
signed.order_hash,
self.builder.build_order_hash(typed_data),
)
encode.assert_called_once()
duplicate_hash.assert_not_called()
Loading