From 0ea3f3436da08bfc4db6dfb6b190fedd024dc0d8 Mon Sep 17 00:00:00 2001 From: Loren Phillips Date: Sat, 6 Dec 2025 21:23:28 -0800 Subject: [PATCH] fix(client): use proper exception instead of assert for US endpoints assert statements get disabled when running python with -O flag which means the region check silently dissapears in production. Replaced with BinanceRegionException that gives callers a proper catchable error type. Affects get_staking_asset_us, stake_asset_us, unstake_asset_us, get_staking_balance_us, get_staking_history_us, get_staking_rewards_history_us --- binance/async_client.py | 12 +-- binance/base_client.py | 12 +++ binance/client.py | 18 ++-- binance/exceptions.py | 19 ++++ tests/test_region_exception.py | 155 +++++++++++++++++++++++++++++++++ 5 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 tests/test_region_exception.py diff --git a/binance/async_client.py b/binance/async_client.py index f0a623f4..5fb0f688 100644 --- a/binance/async_client.py +++ b/binance/async_client.py @@ -1538,13 +1538,13 @@ async def get_personal_left_quota(self, **params): # US Staking Endpoints async def get_staking_asset_us(self, **params): - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "get_staking_asset_us") return await self._request_margin_api("get", "staking/asset", True, data=params) get_staking_asset_us.__doc__ = Client.get_staking_asset_us.__doc__ async def stake_asset_us(self, **params): - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "stake_asset_us") return await self._request_margin_api( "post", "staking/stake", True, data=params ) @@ -1552,7 +1552,7 @@ async def stake_asset_us(self, **params): stake_asset_us.__doc__ = Client.stake_asset_us.__doc__ async def unstake_asset_us(self, **params): - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "unstake_asset_us") return await self._request_margin_api( "post", "staking/unstake", True, data=params ) @@ -1560,7 +1560,7 @@ async def unstake_asset_us(self, **params): unstake_asset_us.__doc__ = Client.unstake_asset_us.__doc__ async def get_staking_balance_us(self, **params): - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "get_staking_balance_us") return await self._request_margin_api( "get", "staking/stakingBalance", True, data=params ) @@ -1568,7 +1568,7 @@ async def get_staking_balance_us(self, **params): get_staking_balance_us.__doc__ = Client.get_staking_balance_us.__doc__ async def get_staking_history_us(self, **params): - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "get_staking_history_us") return await self._request_margin_api( "get", "staking/history", True, data=params ) @@ -1576,7 +1576,7 @@ async def get_staking_history_us(self, **params): get_staking_history_us.__doc__ = Client.get_staking_history_us.__doc__ async def get_staking_rewards_history_us(self, **params): - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "get_staking_rewards_history_us") return await self._request_margin_api( "get", "staking/stakingRewardsHistory", True, data=params ) diff --git a/binance/base_client.py b/binance/base_client.py index b7ae76d7..649dca51 100644 --- a/binance/base_client.py +++ b/binance/base_client.py @@ -350,6 +350,18 @@ def convert_to_dict(list_tuples): dictionary = dict((key, value) for key, value in list_tuples) return dictionary + def _require_tld(self, required_tld: str, endpoint_name: str = "endpoint") -> None: + """Validate client is configured for required TLD. + + :param required_tld: The required TLD (e.g., "us") + :param endpoint_name: Description of the endpoint for error messages + :raises BinanceRegionException: If the client TLD doesn't match + """ + if self.tld != required_tld: + from binance.exceptions import BinanceRegionException + + raise BinanceRegionException(required_tld, self.tld, endpoint_name) + def _ed25519_signature(self, query_string: str): assert self.PRIVATE_KEY res = b64encode( diff --git a/binance/client.py b/binance/client.py index 5774469e..e8163a9d 100755 --- a/binance/client.py +++ b/binance/client.py @@ -6261,8 +6261,9 @@ def get_staking_asset_us(self, **params): https://docs.binance.us/#get-staking-asset-information + :raises BinanceRegionException: If client is not configured for binance.us """ - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "get_staking_asset_us") return self._request_margin_api("get", "staking/asset", True, data=params) def stake_asset_us(self, **params): @@ -6270,8 +6271,9 @@ def stake_asset_us(self, **params): https://docs.binance.us/#stake-asset + :raises BinanceRegionException: If client is not configured for binance.us """ - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "stake_asset_us") return self._request_margin_api("post", "staking/stake", True, data=params) def unstake_asset_us(self, **params): @@ -6279,8 +6281,9 @@ def unstake_asset_us(self, **params): https://docs.binance.us/#unstake-asset + :raises BinanceRegionException: If client is not configured for binance.us """ - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "unstake_asset_us") return self._request_margin_api("post", "staking/unstake", True, data=params) def get_staking_balance_us(self, **params): @@ -6288,8 +6291,9 @@ def get_staking_balance_us(self, **params): https://docs.binance.us/#get-staking-balance + :raises BinanceRegionException: If client is not configured for binance.us """ - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "get_staking_balance_us") return self._request_margin_api( "get", "staking/stakingBalance", True, data=params ) @@ -6299,8 +6303,9 @@ def get_staking_history_us(self, **params): https://docs.binance.us/#get-staking-history + :raises BinanceRegionException: If client is not configured for binance.us """ - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "get_staking_history_us") return self._request_margin_api("get", "staking/history", True, data=params) def get_staking_rewards_history_us(self, **params): @@ -6308,8 +6313,9 @@ def get_staking_rewards_history_us(self, **params): https://docs.binance.us/#get-staking-rewards-history + :raises BinanceRegionException: If client is not configured for binance.us """ - assert self.tld == "us", "Endpoint only available on binance.us" + self._require_tld("us", "get_staking_rewards_history_us") return self._request_margin_api( "get", "staking/stakingRewardsHistory", True, data=params ) diff --git a/binance/exceptions.py b/binance/exceptions.py index ec84d651..decec59b 100644 --- a/binance/exceptions.py +++ b/binance/exceptions.py @@ -93,3 +93,22 @@ def __init__(self, value): class UnknownDateFormat(Exception): ... + + +class BinanceRegionException(Exception): + """Raised when using a region-specific endpoint with incompatible client.""" + + def __init__( + self, required_tld: str, actual_tld: str, endpoint_name: str = "endpoint" + ): + self.required_tld = required_tld + self.actual_tld = actual_tld + self.endpoint_name = endpoint_name + self.message = ( + f"{endpoint_name} is only available on binance.{required_tld}, " + f"but client is configured for binance.{actual_tld}" + ) + super().__init__(self.message) + + def __str__(self): + return f"BinanceRegionException: {self.message}" diff --git a/tests/test_region_exception.py b/tests/test_region_exception.py new file mode 100644 index 00000000..4e830d76 --- /dev/null +++ b/tests/test_region_exception.py @@ -0,0 +1,155 @@ +"""Tests for BinanceRegionException and region validation.""" + +import pytest +from binance.client import Client +from binance.async_client import AsyncClient +from binance.exceptions import BinanceRegionException + + +class TestBinanceRegionException: + """Tests for the BinanceRegionException class itself.""" + + def test_exception_attributes(self): + """Test that exception has correct attributes.""" + exc = BinanceRegionException("us", "com", "test_endpoint") + assert exc.required_tld == "us" + assert exc.actual_tld == "com" + assert exc.endpoint_name == "test_endpoint" + + def test_exception_message_format(self): + """Test that exception message is properly formatted.""" + exc = BinanceRegionException("us", "com", "get_staking_asset_us") + assert "binance.us" in str(exc) + assert "binance.com" in str(exc) + assert "get_staking_asset_us" in str(exc) + + def test_exception_default_endpoint_name(self): + """Test that endpoint_name defaults to 'endpoint'.""" + exc = BinanceRegionException("us", "com") + assert exc.endpoint_name == "endpoint" + assert "endpoint is only available" in str(exc) + + +class TestSyncClientRegionValidation: + """Tests for region validation in synchronous Client.""" + + def test_get_staking_asset_us_wrong_tld(self): + """Test that get_staking_asset_us raises exception for non-US client.""" + client = Client("test_key", "test_secret", tld="com", ping=False) + with pytest.raises(BinanceRegionException) as exc_info: + client.get_staking_asset_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.actual_tld == "com" + assert exc_info.value.endpoint_name == "get_staking_asset_us" + + def test_stake_asset_us_wrong_tld(self): + """Test that stake_asset_us raises exception for non-US client.""" + client = Client("test_key", "test_secret", tld="com", ping=False) + with pytest.raises(BinanceRegionException) as exc_info: + client.stake_asset_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "stake_asset_us" + + def test_unstake_asset_us_wrong_tld(self): + """Test that unstake_asset_us raises exception for non-US client.""" + client = Client("test_key", "test_secret", tld="com", ping=False) + with pytest.raises(BinanceRegionException) as exc_info: + client.unstake_asset_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "unstake_asset_us" + + def test_get_staking_balance_us_wrong_tld(self): + """Test that get_staking_balance_us raises exception for non-US client.""" + client = Client("test_key", "test_secret", tld="com", ping=False) + with pytest.raises(BinanceRegionException) as exc_info: + client.get_staking_balance_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "get_staking_balance_us" + + def test_get_staking_history_us_wrong_tld(self): + """Test that get_staking_history_us raises exception for non-US client.""" + client = Client("test_key", "test_secret", tld="com", ping=False) + with pytest.raises(BinanceRegionException) as exc_info: + client.get_staking_history_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "get_staking_history_us" + + def test_get_staking_rewards_history_us_wrong_tld(self): + """Test that get_staking_rewards_history_us raises exception for non-US client.""" + client = Client("test_key", "test_secret", tld="com", ping=False) + with pytest.raises(BinanceRegionException) as exc_info: + client.get_staking_rewards_history_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "get_staking_rewards_history_us" + + +@pytest.mark.asyncio +class TestAsyncClientRegionValidation: + """Tests for region validation in asynchronous AsyncClient.""" + + async def test_get_staking_asset_us_wrong_tld_async(self): + """Test that async get_staking_asset_us raises exception for non-US client.""" + client = AsyncClient("test_key", "test_secret", tld="com") + try: + with pytest.raises(BinanceRegionException) as exc_info: + await client.get_staking_asset_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.actual_tld == "com" + assert exc_info.value.endpoint_name == "get_staking_asset_us" + finally: + await client.close_connection() + + async def test_stake_asset_us_wrong_tld_async(self): + """Test that async stake_asset_us raises exception for non-US client.""" + client = AsyncClient("test_key", "test_secret", tld="com") + try: + with pytest.raises(BinanceRegionException) as exc_info: + await client.stake_asset_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "stake_asset_us" + finally: + await client.close_connection() + + async def test_unstake_asset_us_wrong_tld_async(self): + """Test that async unstake_asset_us raises exception for non-US client.""" + client = AsyncClient("test_key", "test_secret", tld="com") + try: + with pytest.raises(BinanceRegionException) as exc_info: + await client.unstake_asset_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "unstake_asset_us" + finally: + await client.close_connection() + + async def test_get_staking_balance_us_wrong_tld_async(self): + """Test that async get_staking_balance_us raises exception for non-US client.""" + client = AsyncClient("test_key", "test_secret", tld="com") + try: + with pytest.raises(BinanceRegionException) as exc_info: + await client.get_staking_balance_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "get_staking_balance_us" + finally: + await client.close_connection() + + async def test_get_staking_history_us_wrong_tld_async(self): + """Test that async get_staking_history_us raises exception for non-US client.""" + client = AsyncClient("test_key", "test_secret", tld="com") + try: + with pytest.raises(BinanceRegionException) as exc_info: + await client.get_staking_history_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "get_staking_history_us" + finally: + await client.close_connection() + + async def test_get_staking_rewards_history_us_wrong_tld_async(self): + """Test that async get_staking_rewards_history_us raises exception for non-US client.""" + client = AsyncClient("test_key", "test_secret", tld="com") + try: + with pytest.raises(BinanceRegionException) as exc_info: + await client.get_staking_rewards_history_us() + assert exc_info.value.required_tld == "us" + assert exc_info.value.endpoint_name == "get_staking_rewards_history_us" + finally: + await client.close_connection()