Skip to content

Commit a0b9b55

Browse files
authored
Merge pull request #245 from opentensor/release/1.5.13
Release/1.5.13
2 parents c9f7924 + 306ed0d commit a0b9b55

File tree

5 files changed

+146
-27
lines changed

5 files changed

+146
-27
lines changed

.github/workflows/check-sdk-tests.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,9 @@ jobs:
209209
- name: Checkout PR branch in async-substrate-interface
210210
working-directory: ${{ github.workspace }}/async-substrate-interface
211211
run: |
212-
git fetch origin ${{ github.event.pull_request.head.ref }}
213-
git checkout ${{ github.event.pull_request.head.ref }}
212+
BRANCH="${{ github.event.pull_request.head.ref || github.ref_name }}"
213+
git fetch origin $BRANCH
214+
git checkout $BRANCH
214215
echo "Current branch: $(git rev-parse --abbrev-ref HEAD)"
215216
216217
- name: Install async-substrate-interface with dev dependencies

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
2-
## 1.5.12 /2025-11-167
2+
## 1.5.13 /2025-12-01
3+
* Update `Checkout PR branch in async-substrate-interface` step by @basfroman in https://github.com/opentensor/async-substrate-interface/pull/240
4+
* No continual reconnection without cause by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/241
5+
* Feat: Add support for MeV shield extrinsics by @ibraheem-abe in https://github.com/opentensor/async-substrate-interface/pull/242
6+
* Handle subscription failures from substrate by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/243
7+
8+
9+
**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.5.12...v1.5.13
10+
11+
## 1.5.12 /2025-11-17
312
* RecursionError in `_wait_with_activity_timeout` with concurrent tasks by @Arthurdw in https://github.com/opentensor/async-substrate-interface/pull/238
413
* Improved Test Running + Race Condition Catch by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/236
514

async_substrate_interface/async_substrate.py

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -274,14 +274,15 @@ async def process_events(self):
274274
has_transaction_fee_paid_event = True
275275

276276
# Process other events
277+
possible_success = False
277278
for event in await self.triggered_events:
279+
# TODO make this more readable
278280
# Check events
279281
if (
280282
event["event"]["module_id"] == "System"
281283
and event["event"]["event_id"] == "ExtrinsicSuccess"
282284
):
283-
self.__is_success = True
284-
self.__error_message = None
285+
possible_success = True
285286

286287
if "dispatch_info" in event["event"]["attributes"]:
287288
self.__weight = event["event"]["attributes"]["dispatch_info"][
@@ -294,13 +295,26 @@ async def process_events(self):
294295
elif (
295296
event["event"]["module_id"] == "System"
296297
and event["event"]["event_id"] == "ExtrinsicFailed"
298+
) or (
299+
event["event"]["module_id"] == "MevShield"
300+
and event["event"]["event_id"] == "DecryptedRejected"
297301
):
302+
possible_success = False
298303
self.__is_success = False
299304

300-
dispatch_info = event["event"]["attributes"]["dispatch_info"]
301-
dispatch_error = event["event"]["attributes"]["dispatch_error"]
302-
303-
self.__weight = dispatch_info["weight"]
305+
if event["event"]["module_id"] == "System":
306+
dispatch_info = event["event"]["attributes"]["dispatch_info"]
307+
dispatch_error = event["event"]["attributes"]["dispatch_error"]
308+
self.__weight = dispatch_info["weight"]
309+
else:
310+
# MEV shield extrinsics
311+
dispatch_info = event["event"]["attributes"]["reason"][
312+
"post_info"
313+
]
314+
dispatch_error = event["event"]["attributes"]["reason"]["error"]
315+
self.__weight = event["event"]["attributes"]["reason"][
316+
"post_info"
317+
]["actual_weight"]
304318

305319
if "Module" in dispatch_error:
306320
if isinstance(dispatch_error["Module"], tuple):
@@ -365,7 +379,13 @@ async def process_events(self):
365379
event["event"]["module_id"] == "Balances"
366380
and event["event"]["event_id"] == "Deposit"
367381
):
368-
self.__total_fee_amount += event.value["attributes"]["amount"]
382+
self.__total_fee_amount += event["event"]["attributes"][
383+
"amount"
384+
]
385+
if possible_success is True and self.__error_message is None:
386+
# we delay the positive setting of the __is_success flag until we have finished iteration of the
387+
# events and have ensured nothing has set an error message
388+
self.__is_success = True
369389

370390
@property
371391
async def is_success(self) -> bool:
@@ -833,7 +853,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
833853
pass
834854
if self.ws is not None:
835855
self._exit_task = asyncio.create_task(self._exit_with_timer())
836-
self._attempts = 0
856+
self._attempts = 0
837857

838858
async def _exit_with_timer(self):
839859
"""
@@ -891,12 +911,22 @@ async def _start_receiving(self, ws: ClientConnection) -> Exception:
891911
logger.debug("Starting receiving task")
892912
try:
893913
while True:
894-
recd = await self._wait_with_activity_timeout(
895-
ws.recv(decode=False), self.retry_timeout
896-
)
897-
await self._reset_activity_timer()
898-
self._attempts = 0
899-
await self._recv(recd)
914+
try:
915+
recd = await self._wait_with_activity_timeout(
916+
ws.recv(decode=False), self.retry_timeout
917+
)
918+
await self._reset_activity_timer()
919+
self._attempts = 0
920+
await self._recv(recd)
921+
except TimeoutError:
922+
if (
923+
self._waiting_for_response <= 0
924+
or self._sending.qsize() == 0
925+
or len(self._inflight) == 0
926+
or len(self._received_subscriptions) == 0
927+
):
928+
# if there's nothing in a queue, we really have no reason to have this, so we continue to wait
929+
continue
900930
except websockets.exceptions.ConnectionClosedOK as e:
901931
logger.debug("ConnectionClosedOK")
902932
return e
@@ -939,7 +969,14 @@ async def _start_sending(self, ws) -> Exception:
939969
if not isinstance(
940970
e, (asyncio.TimeoutError, TimeoutError, ConnectionClosed)
941971
):
942-
logger.exception("Websocket sending exception", exc_info=e)
972+
logger.exception(
973+
f"Websocket sending exception; "
974+
f"sending: {self._sending.qsize()}; "
975+
f"waiting_for_response: {self._waiting_for_response}; "
976+
f"inflight: {len(self._inflight)}; "
977+
f"subscriptions: {len(self._received_subscriptions)};",
978+
exc_info=e,
979+
)
943980
if to_send is not None:
944981
to_send_ = json.loads(to_send)
945982
self._received[to_send_["id"]].set_exception(e)
@@ -3987,6 +4024,32 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]:
39874024
message_result = {
39884025
k.lower(): v for k, v in message["params"]["result"].items()
39894026
}
4027+
# check for any subscription indicators of failure
4028+
failure_message = None
4029+
if "usurped" in message_result:
4030+
failure_message = (
4031+
f"Subscription {subscription_id} usurped: {message_result}"
4032+
)
4033+
if "retracted" in message_result:
4034+
failure_message = (
4035+
f"Subscription {subscription_id} retracted: {message_result}"
4036+
)
4037+
if "finalitytimeout" in message_result:
4038+
failure_message = f"Subscription {subscription_id} finalityTimeout: {message_result}"
4039+
if "dropped" in message_result:
4040+
failure_message = (
4041+
f"Subscription {subscription_id} dropped: {message_result}"
4042+
)
4043+
if "invalid" in message_result:
4044+
failure_message = (
4045+
f"Subscription {subscription_id} invalid: {message_result}"
4046+
)
4047+
4048+
if failure_message is not None:
4049+
async with self.ws as ws:
4050+
await ws.unsubscribe(subscription_id)
4051+
logger.error(failure_message)
4052+
raise SubstrateRequestException(failure_message)
39904053

39914054
if "finalized" in message_result and wait_for_finalization:
39924055
logger.debug("Extrinsic finalized. Unsubscribing.")
@@ -3998,7 +4061,7 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]:
39984061
"finalized": True,
39994062
}, True
40004063
elif (
4001-
any(x in message_result for x in ["inblock", "inBlock"])
4064+
"inblock" in message_result
40024065
and wait_for_inclusion
40034066
and not wait_for_finalization
40044067
):

async_substrate_interface/sync_substrate.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,15 @@ def process_events(self):
235235
has_transaction_fee_paid_event = True
236236

237237
# Process other events
238+
possible_success = False
238239
for event in self.triggered_events:
240+
# TODO make this more readable
239241
# Check events
240242
if (
241243
event["event"]["module_id"] == "System"
242244
and event["event"]["event_id"] == "ExtrinsicSuccess"
243245
):
244-
self.__is_success = True
245-
self.__error_message = None
246+
possible_success = True
246247

247248
if "dispatch_info" in event["event"]["attributes"]:
248249
self.__weight = event["event"]["attributes"]["dispatch_info"][
@@ -255,13 +256,26 @@ def process_events(self):
255256
elif (
256257
event["event"]["module_id"] == "System"
257258
and event["event"]["event_id"] == "ExtrinsicFailed"
259+
) or (
260+
event["event"]["module_id"] == "MevShield"
261+
and event["event"]["event_id"] == "DecryptedRejected"
258262
):
263+
possible_success = False
259264
self.__is_success = False
260265

261-
dispatch_info = event["event"]["attributes"]["dispatch_info"]
262-
dispatch_error = event["event"]["attributes"]["dispatch_error"]
263-
264-
self.__weight = dispatch_info["weight"]
266+
if event["event"]["module_id"] == "System":
267+
dispatch_info = event["event"]["attributes"]["dispatch_info"]
268+
dispatch_error = event["event"]["attributes"]["dispatch_error"]
269+
self.__weight = dispatch_info["weight"]
270+
else:
271+
# MEV shield extrinsics
272+
dispatch_info = event["event"]["attributes"]["reason"][
273+
"post_info"
274+
]
275+
dispatch_error = event["event"]["attributes"]["reason"]["error"]
276+
self.__weight = event["event"]["attributes"]["reason"][
277+
"post_info"
278+
]["actual_weight"]
265279

266280
if "Module" in dispatch_error:
267281
if isinstance(dispatch_error["Module"], tuple):
@@ -318,7 +332,13 @@ def process_events(self):
318332
event["event"]["module_id"] == "Balances"
319333
and event["event"]["event_id"] == "Deposit"
320334
):
321-
self.__total_fee_amount += event.value["attributes"]["amount"]
335+
self.__total_fee_amount += event["event"]["attributes"][
336+
"amount"
337+
]
338+
if possible_success is True and self.__error_message is None:
339+
# we delay the positive setting of the __is_success flag until we have finished iteration of the
340+
# events and have ensured nothing has set an error message
341+
self.__is_success = True
322342

323343
@property
324344
def is_success(self) -> bool:
@@ -3170,6 +3190,32 @@ def result_handler(message: dict, subscription_id) -> tuple[dict, bool]:
31703190
k.lower(): v for k, v in message["params"]["result"].items()
31713191
}
31723192

3193+
# check for any subscription indicators of failure
3194+
failure_message = None
3195+
if "usurped" in message_result:
3196+
failure_message = (
3197+
f"Subscription {subscription_id} usurped: {message_result}"
3198+
)
3199+
if "retracted" in message_result:
3200+
failure_message = (
3201+
f"Subscription {subscription_id} retracted: {message_result}"
3202+
)
3203+
if "finalitytimeout" in message_result:
3204+
failure_message = f"Subscription {subscription_id} finalityTimeout: {message_result}"
3205+
if "dropped" in message_result:
3206+
failure_message = (
3207+
f"Subscription {subscription_id} dropped: {message_result}"
3208+
)
3209+
if "invalid" in message_result:
3210+
failure_message = (
3211+
f"Subscription {subscription_id} invalid: {message_result}"
3212+
)
3213+
3214+
if failure_message is not None:
3215+
self.rpc_request("author_unwatchExtrinsic", [subscription_id])
3216+
logger.error(failure_message)
3217+
raise SubstrateRequestException(failure_message)
3218+
31733219
if "finalized" in message_result and wait_for_finalization:
31743220
# Created as a task because we don't actually care about the result
31753221
# TODO change this logic

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "async-substrate-interface"
3-
version = "1.5.12"
3+
version = "1.5.13"
44
description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface"
55
readme = "README.md"
66
license = { file = "LICENSE" }

0 commit comments

Comments
 (0)