Skip to content

Commit 8e536e6

Browse files
fix(client): use proper exception instead of assert for US endpoints (#1641)
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
1 parent e04e2c7 commit 8e536e6

File tree

5 files changed

+204
-12
lines changed

5 files changed

+204
-12
lines changed

binance/async_client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,45 +1600,45 @@ async def get_personal_left_quota(self, **params):
16001600
get_personal_left_quota.__doc__ = Client.get_personal_left_quota.__doc__
16011601

16021602
async def get_staking_asset_us(self, **params):
1603-
assert self.tld == "us", "Endpoint only available on binance.us"
1603+
self._require_tld("us", "get_staking_asset_us")
16041604
return await self._request_margin_api("get", "staking/asset", True, data=params)
16051605

16061606
get_staking_asset_us.__doc__ = Client.get_staking_asset_us.__doc__
16071607

16081608
async def stake_asset_us(self, **params):
1609-
assert self.tld == "us", "Endpoint only available on binance.us"
1609+
self._require_tld("us", "stake_asset_us")
16101610
return await self._request_margin_api(
16111611
"post", "staking/stake", True, data=params
16121612
)
16131613

16141614
stake_asset_us.__doc__ = Client.stake_asset_us.__doc__
16151615

16161616
async def unstake_asset_us(self, **params):
1617-
assert self.tld == "us", "Endpoint only available on binance.us"
1617+
self._require_tld("us", "unstake_asset_us")
16181618
return await self._request_margin_api(
16191619
"post", "staking/unstake", True, data=params
16201620
)
16211621

16221622
unstake_asset_us.__doc__ = Client.unstake_asset_us.__doc__
16231623

16241624
async def get_staking_balance_us(self, **params):
1625-
assert self.tld == "us", "Endpoint only available on binance.us"
1625+
self._require_tld("us", "get_staking_balance_us")
16261626
return await self._request_margin_api(
16271627
"get", "staking/stakingBalance", True, data=params
16281628
)
16291629

16301630
get_staking_balance_us.__doc__ = Client.get_staking_balance_us.__doc__
16311631

16321632
async def get_staking_history_us(self, **params):
1633-
assert self.tld == "us", "Endpoint only available on binance.us"
1633+
self._require_tld("us", "get_staking_history_us")
16341634
return await self._request_margin_api(
16351635
"get", "staking/history", True, data=params
16361636
)
16371637

16381638
get_staking_history_us.__doc__ = Client.get_staking_history_us.__doc__
16391639

16401640
async def get_staking_rewards_history_us(self, **params):
1641-
assert self.tld == "us", "Endpoint only available on binance.us"
1641+
self._require_tld("us", "get_staking_rewards_history_us")
16421642
return await self._request_margin_api(
16431643
"get", "staking/stakingRewardsHistory", True, data=params
16441644
)

binance/base_client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,18 @@ def convert_to_dict(list_tuples):
350350
dictionary = dict((key, value) for key, value in list_tuples)
351351
return dictionary
352352

353+
def _require_tld(self, required_tld: str, endpoint_name: str = "endpoint") -> None:
354+
"""Validate client is configured for required TLD.
355+
356+
:param required_tld: The required TLD (e.g., "us")
357+
:param endpoint_name: Description of the endpoint for error messages
358+
:raises BinanceRegionException: If the client TLD doesn't match
359+
"""
360+
if self.tld != required_tld:
361+
from binance.exceptions import BinanceRegionException
362+
363+
raise BinanceRegionException(required_tld, self.tld, endpoint_name)
364+
353365
def _ed25519_signature(self, query_string: str):
354366
assert self.PRIVATE_KEY
355367
res = b64encode(

binance/client.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6261,35 +6261,39 @@ def get_staking_asset_us(self, **params):
62616261

62626262
https://docs.binance.us/#get-staking-asset-information
62636263

6264+
:raises BinanceRegionException: If client is not configured for binance.us
62646265
"""
6265-
assert self.tld == "us", "Endpoint only available on binance.us"
6266+
self._require_tld("us", "get_staking_asset_us")
62666267
return self._request_margin_api("get", "staking/asset", True, data=params)
62676268

62686269
def stake_asset_us(self, **params):
62696270
"""Stake a supported asset.
62706271

62716272
https://docs.binance.us/#stake-asset
62726273

6274+
:raises BinanceRegionException: If client is not configured for binance.us
62736275
"""
6274-
assert self.tld == "us", "Endpoint only available on binance.us"
6276+
self._require_tld("us", "stake_asset_us")
62756277
return self._request_margin_api("post", "staking/stake", True, data=params)
62766278

62776279
def unstake_asset_us(self, **params):
62786280
"""Unstake a staked asset
62796281

62806282
https://docs.binance.us/#unstake-asset
62816283

6284+
:raises BinanceRegionException: If client is not configured for binance.us
62826285
"""
6283-
assert self.tld == "us", "Endpoint only available on binance.us"
6286+
self._require_tld("us", "unstake_asset_us")
62846287
return self._request_margin_api("post", "staking/unstake", True, data=params)
62856288

62866289
def get_staking_balance_us(self, **params):
62876290
"""Get staking balance
62886291

62896292
https://docs.binance.us/#get-staking-balance
62906293

6294+
:raises BinanceRegionException: If client is not configured for binance.us
62916295
"""
6292-
assert self.tld == "us", "Endpoint only available on binance.us"
6296+
self._require_tld("us", "get_staking_balance_us")
62936297
return self._request_margin_api(
62946298
"get", "staking/stakingBalance", True, data=params
62956299
)
@@ -6299,17 +6303,19 @@ def get_staking_history_us(self, **params):
62996303

63006304
https://docs.binance.us/#get-staking-history
63016305

6306+
:raises BinanceRegionException: If client is not configured for binance.us
63026307
"""
6303-
assert self.tld == "us", "Endpoint only available on binance.us"
6308+
self._require_tld("us", "get_staking_history_us")
63046309
return self._request_margin_api("get", "staking/history", True, data=params)
63056310

63066311
def get_staking_rewards_history_us(self, **params):
63076312
"""Get staking rewards history for an asset(or assets) within a given time range.
63086313

63096314
https://docs.binance.us/#get-staking-rewards-history
63106315

6316+
:raises BinanceRegionException: If client is not configured for binance.us
63116317
"""
6312-
assert self.tld == "us", "Endpoint only available on binance.us"
6318+
self._require_tld("us", "get_staking_rewards_history_us")
63136319
return self._request_margin_api(
63146320
"get", "staking/stakingRewardsHistory", True, data=params
63156321
)

binance/exceptions.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,22 @@ def __init__(self, value):
9393

9494
class UnknownDateFormat(Exception):
9595
...
96+
97+
98+
class BinanceRegionException(Exception):
99+
"""Raised when using a region-specific endpoint with incompatible client."""
100+
101+
def __init__(
102+
self, required_tld: str, actual_tld: str, endpoint_name: str = "endpoint"
103+
):
104+
self.required_tld = required_tld
105+
self.actual_tld = actual_tld
106+
self.endpoint_name = endpoint_name
107+
self.message = (
108+
f"{endpoint_name} is only available on binance.{required_tld}, "
109+
f"but client is configured for binance.{actual_tld}"
110+
)
111+
super().__init__(self.message)
112+
113+
def __str__(self):
114+
return f"BinanceRegionException: {self.message}"

tests/test_region_exception.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""Tests for BinanceRegionException and region validation."""
2+
3+
import pytest
4+
from binance.client import Client
5+
from binance.async_client import AsyncClient
6+
from binance.exceptions import BinanceRegionException
7+
8+
9+
class TestBinanceRegionException:
10+
"""Tests for the BinanceRegionException class itself."""
11+
12+
def test_exception_attributes(self):
13+
"""Test that exception has correct attributes."""
14+
exc = BinanceRegionException("us", "com", "test_endpoint")
15+
assert exc.required_tld == "us"
16+
assert exc.actual_tld == "com"
17+
assert exc.endpoint_name == "test_endpoint"
18+
19+
def test_exception_message_format(self):
20+
"""Test that exception message is properly formatted."""
21+
exc = BinanceRegionException("us", "com", "get_staking_asset_us")
22+
assert "binance.us" in str(exc)
23+
assert "binance.com" in str(exc)
24+
assert "get_staking_asset_us" in str(exc)
25+
26+
def test_exception_default_endpoint_name(self):
27+
"""Test that endpoint_name defaults to 'endpoint'."""
28+
exc = BinanceRegionException("us", "com")
29+
assert exc.endpoint_name == "endpoint"
30+
assert "endpoint is only available" in str(exc)
31+
32+
33+
class TestSyncClientRegionValidation:
34+
"""Tests for region validation in synchronous Client."""
35+
36+
def test_get_staking_asset_us_wrong_tld(self):
37+
"""Test that get_staking_asset_us raises exception for non-US client."""
38+
client = Client("test_key", "test_secret", tld="com", ping=False)
39+
with pytest.raises(BinanceRegionException) as exc_info:
40+
client.get_staking_asset_us()
41+
assert exc_info.value.required_tld == "us"
42+
assert exc_info.value.actual_tld == "com"
43+
assert exc_info.value.endpoint_name == "get_staking_asset_us"
44+
45+
def test_stake_asset_us_wrong_tld(self):
46+
"""Test that stake_asset_us raises exception for non-US client."""
47+
client = Client("test_key", "test_secret", tld="com", ping=False)
48+
with pytest.raises(BinanceRegionException) as exc_info:
49+
client.stake_asset_us()
50+
assert exc_info.value.required_tld == "us"
51+
assert exc_info.value.endpoint_name == "stake_asset_us"
52+
53+
def test_unstake_asset_us_wrong_tld(self):
54+
"""Test that unstake_asset_us raises exception for non-US client."""
55+
client = Client("test_key", "test_secret", tld="com", ping=False)
56+
with pytest.raises(BinanceRegionException) as exc_info:
57+
client.unstake_asset_us()
58+
assert exc_info.value.required_tld == "us"
59+
assert exc_info.value.endpoint_name == "unstake_asset_us"
60+
61+
def test_get_staking_balance_us_wrong_tld(self):
62+
"""Test that get_staking_balance_us raises exception for non-US client."""
63+
client = Client("test_key", "test_secret", tld="com", ping=False)
64+
with pytest.raises(BinanceRegionException) as exc_info:
65+
client.get_staking_balance_us()
66+
assert exc_info.value.required_tld == "us"
67+
assert exc_info.value.endpoint_name == "get_staking_balance_us"
68+
69+
def test_get_staking_history_us_wrong_tld(self):
70+
"""Test that get_staking_history_us raises exception for non-US client."""
71+
client = Client("test_key", "test_secret", tld="com", ping=False)
72+
with pytest.raises(BinanceRegionException) as exc_info:
73+
client.get_staking_history_us()
74+
assert exc_info.value.required_tld == "us"
75+
assert exc_info.value.endpoint_name == "get_staking_history_us"
76+
77+
def test_get_staking_rewards_history_us_wrong_tld(self):
78+
"""Test that get_staking_rewards_history_us raises exception for non-US client."""
79+
client = Client("test_key", "test_secret", tld="com", ping=False)
80+
with pytest.raises(BinanceRegionException) as exc_info:
81+
client.get_staking_rewards_history_us()
82+
assert exc_info.value.required_tld == "us"
83+
assert exc_info.value.endpoint_name == "get_staking_rewards_history_us"
84+
85+
86+
@pytest.mark.asyncio
87+
class TestAsyncClientRegionValidation:
88+
"""Tests for region validation in asynchronous AsyncClient."""
89+
90+
async def test_get_staking_asset_us_wrong_tld_async(self):
91+
"""Test that async get_staking_asset_us raises exception for non-US client."""
92+
client = AsyncClient("test_key", "test_secret", tld="com")
93+
try:
94+
with pytest.raises(BinanceRegionException) as exc_info:
95+
await client.get_staking_asset_us()
96+
assert exc_info.value.required_tld == "us"
97+
assert exc_info.value.actual_tld == "com"
98+
assert exc_info.value.endpoint_name == "get_staking_asset_us"
99+
finally:
100+
await client.close_connection()
101+
102+
async def test_stake_asset_us_wrong_tld_async(self):
103+
"""Test that async stake_asset_us raises exception for non-US client."""
104+
client = AsyncClient("test_key", "test_secret", tld="com")
105+
try:
106+
with pytest.raises(BinanceRegionException) as exc_info:
107+
await client.stake_asset_us()
108+
assert exc_info.value.required_tld == "us"
109+
assert exc_info.value.endpoint_name == "stake_asset_us"
110+
finally:
111+
await client.close_connection()
112+
113+
async def test_unstake_asset_us_wrong_tld_async(self):
114+
"""Test that async unstake_asset_us raises exception for non-US client."""
115+
client = AsyncClient("test_key", "test_secret", tld="com")
116+
try:
117+
with pytest.raises(BinanceRegionException) as exc_info:
118+
await client.unstake_asset_us()
119+
assert exc_info.value.required_tld == "us"
120+
assert exc_info.value.endpoint_name == "unstake_asset_us"
121+
finally:
122+
await client.close_connection()
123+
124+
async def test_get_staking_balance_us_wrong_tld_async(self):
125+
"""Test that async get_staking_balance_us raises exception for non-US client."""
126+
client = AsyncClient("test_key", "test_secret", tld="com")
127+
try:
128+
with pytest.raises(BinanceRegionException) as exc_info:
129+
await client.get_staking_balance_us()
130+
assert exc_info.value.required_tld == "us"
131+
assert exc_info.value.endpoint_name == "get_staking_balance_us"
132+
finally:
133+
await client.close_connection()
134+
135+
async def test_get_staking_history_us_wrong_tld_async(self):
136+
"""Test that async get_staking_history_us raises exception for non-US client."""
137+
client = AsyncClient("test_key", "test_secret", tld="com")
138+
try:
139+
with pytest.raises(BinanceRegionException) as exc_info:
140+
await client.get_staking_history_us()
141+
assert exc_info.value.required_tld == "us"
142+
assert exc_info.value.endpoint_name == "get_staking_history_us"
143+
finally:
144+
await client.close_connection()
145+
146+
async def test_get_staking_rewards_history_us_wrong_tld_async(self):
147+
"""Test that async get_staking_rewards_history_us raises exception for non-US client."""
148+
client = AsyncClient("test_key", "test_secret", tld="com")
149+
try:
150+
with pytest.raises(BinanceRegionException) as exc_info:
151+
await client.get_staking_rewards_history_us()
152+
assert exc_info.value.required_tld == "us"
153+
assert exc_info.value.endpoint_name == "get_staking_rewards_history_us"
154+
finally:
155+
await client.close_connection()

0 commit comments

Comments
 (0)