From 410bf1c498c6ef7364535f34c75af4dfabf06885 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 17 Nov 2025 12:55:49 -0800 Subject: [PATCH 1/9] update `Checkout PR branch in async-substrate-interface` step --- .github/workflows/check-sdk-tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-sdk-tests.yml b/.github/workflows/check-sdk-tests.yml index b9f2858..cd1832d 100644 --- a/.github/workflows/check-sdk-tests.yml +++ b/.github/workflows/check-sdk-tests.yml @@ -209,8 +209,9 @@ jobs: - name: Checkout PR branch in async-substrate-interface working-directory: ${{ github.workspace }}/async-substrate-interface run: | - git fetch origin ${{ github.event.pull_request.head.ref }} - git checkout ${{ github.event.pull_request.head.ref }} + BRANCH="${{ github.event.pull_request.head.ref || github.ref_name }}" + git fetch origin $BRANCH + git checkout $BRANCH echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" - name: Install async-substrate-interface with dev dependencies From 878dd2afaa06d439084fb7f6bcdea709d6bb5f2c Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 24 Nov 2025 20:03:32 +0200 Subject: [PATCH 2/9] Should not reconnect forever --- async_substrate_interface/async_substrate.py | 33 +++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index c555c7b..af37530 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -833,7 +833,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): pass if self.ws is not None: self._exit_task = asyncio.create_task(self._exit_with_timer()) - self._attempts = 0 + self._attempts = 0 async def _exit_with_timer(self): """ @@ -891,12 +891,22 @@ async def _start_receiving(self, ws: ClientConnection) -> Exception: logger.debug("Starting receiving task") try: while True: - recd = await self._wait_with_activity_timeout( - ws.recv(decode=False), self.retry_timeout - ) - await self._reset_activity_timer() - self._attempts = 0 - await self._recv(recd) + try: + recd = await self._wait_with_activity_timeout( + ws.recv(decode=False), self.retry_timeout + ) + await self._reset_activity_timer() + self._attempts = 0 + await self._recv(recd) + except TimeoutError: + if ( + self._waiting_for_response <= 0 + or self._sending.qsize() == 0 + or len(self._inflight) == 0 + or len(self._received_subscriptions) == 0 + ): + # if there's nothing in a queue, we really have no reason to have this, so we continue to wait + continue except websockets.exceptions.ConnectionClosedOK as e: logger.debug("ConnectionClosedOK") return e @@ -939,7 +949,14 @@ async def _start_sending(self, ws) -> Exception: if not isinstance( e, (asyncio.TimeoutError, TimeoutError, ConnectionClosed) ): - logger.exception("Websocket sending exception", exc_info=e) + logger.exception( + f"Websocket sending exception; " + f"sending: {self._sending.qsize()}; " + f"waiting_for_response: {self._waiting_for_response}; " + f"inflight: {len(self._inflight)}; " + f"subscriptions: {len(self._received_subscriptions)};", + exc_info=e, + ) if to_send is not None: to_send_ = json.loads(to_send) self._received[to_send_["id"]].set_exception(e) From 11bfdf51c2e949ed51e56e042a75ec098b2ea8b9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 20:16:14 -0800 Subject: [PATCH 3/9] adds mev shield handling --- async_substrate_interface/async_substrate.py | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index af37530..5706660 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -280,8 +280,9 @@ async def process_events(self): event["event"]["module_id"] == "System" and event["event"]["event_id"] == "ExtrinsicSuccess" ): - self.__is_success = True - self.__error_message = None + if self.__error_message is None: + self.__is_success = True + self.__error_message = None if "dispatch_info" in event["event"]["attributes"]: self.__weight = event["event"]["attributes"]["dispatch_info"][ @@ -294,13 +295,21 @@ async def process_events(self): elif ( event["event"]["module_id"] == "System" and event["event"]["event_id"] == "ExtrinsicFailed" + ) or ( + event["event"]["module_id"] == "MevShield" + and event["event"]["event_id"] == "DecryptedRejected" ): self.__is_success = False - dispatch_info = event["event"]["attributes"]["dispatch_info"] - dispatch_error = event["event"]["attributes"]["dispatch_error"] - - self.__weight = dispatch_info["weight"] + if event["event"]["module_id"] == "System": + dispatch_info = event["event"]["attributes"]["dispatch_info"] + dispatch_error = event["event"]["attributes"]["dispatch_error"] + self.__weight = dispatch_info["weight"] + else: + # MEV shield extrinsics + dispatch_info = event["event"]["attributes"]["reason"]["post_info"] + dispatch_error = event["event"]["attributes"]["reason"]["error"] + self.__weight = event["event"]["attributes"]["reason"]["post_info"]["actual_weight"] if "Module" in dispatch_error: if isinstance(dispatch_error["Module"], tuple): From 1c09484c3008601c5e7c82a6a6e58b54fafe23d2 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 28 Nov 2025 12:20:38 +0200 Subject: [PATCH 4/9] Handle 'usurped' error from substrate --- async_substrate_interface/async_substrate.py | 11 ++++++++++- async_substrate_interface/sync_substrate.py | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index c555c7b..b491e5a 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -3987,6 +3987,15 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: message_result = { k.lower(): v for k, v in message["params"]["result"].items() } + if "usurped" in message_result: + logger.error( + f"Subscription {subscription_id} usurped: {message_result}" + ) + async with self.ws as ws: + await ws.unsubscribe(subscription_id) + raise SubstrateRequestException( + f"Subscription {subscription_id} usurped: {message_result}" + ) if "finalized" in message_result and wait_for_finalization: logger.debug("Extrinsic finalized. Unsubscribing.") @@ -3998,7 +4007,7 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: "finalized": True, }, True elif ( - any(x in message_result for x in ["inblock", "inBlock"]) + "inblock" in message_result and wait_for_inclusion and not wait_for_finalization ): diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index c175c1f..630d4cd 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -3170,6 +3170,15 @@ def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: k.lower(): v for k, v in message["params"]["result"].items() } + if "usurped" in message_result: + logger.error( + f"Subscription {subscription_id} usurped: {message_result}" + ) + self.rpc_request("author_unwatchExtrinsic", [subscription_id]) + raise SubstrateRequestException( + f"Subscription {subscription_id} usurped: {message_result}" + ) + if "finalized" in message_result and wait_for_finalization: # Created as a task because we don't actually care about the result # TODO change this logic From 8d816138808e443f7f620d5416aa9d95d59d3c5d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 28 Nov 2025 12:32:00 +0200 Subject: [PATCH 5/9] Apply to sync, better handle possible success --- async_substrate_interface/async_substrate.py | 16 ++++++++---- async_substrate_interface/sync_substrate.py | 27 +++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 5706660..c9cdef2 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -274,15 +274,14 @@ async def process_events(self): has_transaction_fee_paid_event = True # Process other events + possible_success = False for event in await self.triggered_events: # Check events if ( event["event"]["module_id"] == "System" and event["event"]["event_id"] == "ExtrinsicSuccess" ): - if self.__error_message is None: - self.__is_success = True - self.__error_message = None + possible_success = True if "dispatch_info" in event["event"]["attributes"]: self.__weight = event["event"]["attributes"]["dispatch_info"][ @@ -299,6 +298,7 @@ async def process_events(self): event["event"]["module_id"] == "MevShield" and event["event"]["event_id"] == "DecryptedRejected" ): + possible_success = False self.__is_success = False if event["event"]["module_id"] == "System": @@ -307,9 +307,13 @@ async def process_events(self): self.__weight = dispatch_info["weight"] else: # MEV shield extrinsics - dispatch_info = event["event"]["attributes"]["reason"]["post_info"] + dispatch_info = event["event"]["attributes"]["reason"][ + "post_info" + ] dispatch_error = event["event"]["attributes"]["reason"]["error"] - self.__weight = event["event"]["attributes"]["reason"]["post_info"]["actual_weight"] + self.__weight = event["event"]["attributes"]["reason"][ + "post_info" + ]["actual_weight"] if "Module" in dispatch_error: if isinstance(dispatch_error["Module"], tuple): @@ -375,6 +379,8 @@ async def process_events(self): and event["event"]["event_id"] == "Deposit" ): self.__total_fee_amount += event.value["attributes"]["amount"] + if possible_success is True and self.__error_message is None: + self.__is_success = True @property async def is_success(self) -> bool: diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index c175c1f..ef1ec3b 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -235,14 +235,14 @@ def process_events(self): has_transaction_fee_paid_event = True # Process other events + possible_success = False for event in self.triggered_events: # Check events if ( event["event"]["module_id"] == "System" and event["event"]["event_id"] == "ExtrinsicSuccess" ): - self.__is_success = True - self.__error_message = None + possible_success = True if "dispatch_info" in event["event"]["attributes"]: self.__weight = event["event"]["attributes"]["dispatch_info"][ @@ -255,13 +255,26 @@ def process_events(self): elif ( event["event"]["module_id"] == "System" and event["event"]["event_id"] == "ExtrinsicFailed" + ) or ( + event["event"]["module_id"] == "MevShield" + and event["event"]["event_id"] == "DecryptedRejected" ): + possible_success = False self.__is_success = False - dispatch_info = event["event"]["attributes"]["dispatch_info"] - dispatch_error = event["event"]["attributes"]["dispatch_error"] - - self.__weight = dispatch_info["weight"] + if event["event"]["module_id"] == "System": + dispatch_info = event["event"]["attributes"]["dispatch_info"] + dispatch_error = event["event"]["attributes"]["dispatch_error"] + self.__weight = dispatch_info["weight"] + else: + # MEV shield extrinsics + dispatch_info = event["event"]["attributes"]["reason"][ + "post_info" + ] + dispatch_error = event["event"]["attributes"]["reason"]["error"] + self.__weight = event["event"]["attributes"]["reason"][ + "post_info" + ]["actual_weight"] if "Module" in dispatch_error: if isinstance(dispatch_error["Module"], tuple): @@ -319,6 +332,8 @@ def process_events(self): and event["event"]["event_id"] == "Deposit" ): self.__total_fee_amount += event.value["attributes"]["amount"] + if possible_success is True and self.__error_message is None: + self.__is_success = True @property def is_success(self) -> bool: From 199e26c518a0410d7dc54fe8f4cb756a28c32616 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 28 Nov 2025 12:49:28 +0200 Subject: [PATCH 6/9] Added comment --- async_substrate_interface/async_substrate.py | 2 ++ async_substrate_interface/sync_substrate.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index c9cdef2..cad2fae 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -380,6 +380,8 @@ async def process_events(self): ): self.__total_fee_amount += event.value["attributes"]["amount"] if possible_success is True and self.__error_message is None: + # we delay the positive setting of the __is_success flag until we have finished iteration of the + # events and have ensured nothing has set an error message self.__is_success = True @property diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index ef1ec3b..bd80d36 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -333,6 +333,8 @@ def process_events(self): ): self.__total_fee_amount += event.value["attributes"]["amount"] if possible_success is True and self.__error_message is None: + # we delay the positive setting of the __is_success flag until we have finished iteration of the + # events and have ensured nothing has set an error message self.__is_success = True @property From 73b65868b4e8d0b1cc48fe2a1a34d61cc15e110f Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 28 Nov 2025 13:14:15 +0200 Subject: [PATCH 7/9] Handle all subscription update failures --- async_substrate_interface/async_substrate.py | 25 ++++++++++++++++---- async_substrate_interface/sync_substrate.py | 25 ++++++++++++++++---- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index b491e5a..25b6791 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -3987,15 +3987,32 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: message_result = { k.lower(): v for k, v in message["params"]["result"].items() } + # check for any subscription indicators of failure + failure_message = None if "usurped" in message_result: - logger.error( + failure_message = ( f"Subscription {subscription_id} usurped: {message_result}" ) + if "retracted" in message_result: + failure_message = ( + f"Subscription {subscription_id} retracted: {message_result}" + ) + if "finalitytimeout" in message_result: + failure_message = f"Subscription {subscription_id} finalityTimeout: {message_result}" + if "dropped" in message_result: + failure_message = ( + f"Subscription {subscription_id} dropped: {message_result}" + ) + if "invalid" in message_result: + failure_message = ( + f"Subscription {subscription_id} invalid: {message_result}" + ) + + if failure_message is not None: async with self.ws as ws: await ws.unsubscribe(subscription_id) - raise SubstrateRequestException( - f"Subscription {subscription_id} usurped: {message_result}" - ) + logger.error(failure_message) + raise SubstrateRequestException(failure_message) if "finalized" in message_result and wait_for_finalization: logger.debug("Extrinsic finalized. Unsubscribing.") diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 630d4cd..a6c0be0 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -3170,14 +3170,31 @@ def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: k.lower(): v for k, v in message["params"]["result"].items() } + # check for any subscription indicators of failure + failure_message = None if "usurped" in message_result: - logger.error( + failure_message = ( f"Subscription {subscription_id} usurped: {message_result}" ) - self.rpc_request("author_unwatchExtrinsic", [subscription_id]) - raise SubstrateRequestException( - f"Subscription {subscription_id} usurped: {message_result}" + if "retracted" in message_result: + failure_message = ( + f"Subscription {subscription_id} retracted: {message_result}" + ) + if "finalitytimeout" in message_result: + failure_message = f"Subscription {subscription_id} finalityTimeout: {message_result}" + if "dropped" in message_result: + failure_message = ( + f"Subscription {subscription_id} dropped: {message_result}" ) + if "invalid" in message_result: + failure_message = ( + f"Subscription {subscription_id} invalid: {message_result}" + ) + + if failure_message is not None: + self.rpc_request("author_unwatchExtrinsic", [subscription_id]) + logger.error(failure_message) + raise SubstrateRequestException(failure_message) if "finalized" in message_result and wait_for_finalization: # Created as a task because we don't actually care about the result From 933084ce88dd0abbd33c8ac328642ebbba765cd8 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 1 Dec 2025 19:10:50 +0200 Subject: [PATCH 8/9] Changelog + version --- CHANGELOG.md | 11 ++++++++++- pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 829f2fc..57433b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog -## 1.5.12 /2025-11-167 +## 1.5.13 /2025-12-01 +* Update `Checkout PR branch in async-substrate-interface` step by @basfroman in https://github.com/opentensor/async-substrate-interface/pull/240 +* No continual reconnection without cause by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/241 +* Feat: Add support for MeV shield extrinsics by @ibraheem-abe in https://github.com/opentensor/async-substrate-interface/pull/242 +* Handle subscription failures from substrate by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/243 + + +**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.5.12...v1.5.13 + +## 1.5.12 /2025-11-17 * RecursionError in `_wait_with_activity_timeout` with concurrent tasks by @Arthurdw in https://github.com/opentensor/async-substrate-interface/pull/238 * Improved Test Running + Race Condition Catch by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/236 diff --git a/pyproject.toml b/pyproject.toml index 7e3536c..a060d67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "async-substrate-interface" -version = "1.5.12" +version = "1.5.13" description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface" readme = "README.md" license = { file = "LICENSE" } From 306ed0d67820e5930b7336e60c7b2bec209159fe Mon Sep 17 00:00:00 2001 From: Ibraheem <165814940+ibraheem-abe@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:50:29 -0700 Subject: [PATCH 9/9] Merge pull request #246 from opentensor/update/balance-deposit-events Update: Type checking in balance deposit event --- async_substrate_interface/async_substrate.py | 5 ++++- async_substrate_interface/sync_substrate.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 6a1de27..a654c1b 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -276,6 +276,7 @@ async def process_events(self): # Process other events possible_success = False for event in await self.triggered_events: + # TODO make this more readable # Check events if ( event["event"]["module_id"] == "System" @@ -378,7 +379,9 @@ async def process_events(self): event["event"]["module_id"] == "Balances" and event["event"]["event_id"] == "Deposit" ): - self.__total_fee_amount += event.value["attributes"]["amount"] + self.__total_fee_amount += event["event"]["attributes"][ + "amount" + ] if possible_success is True and self.__error_message is None: # we delay the positive setting of the __is_success flag until we have finished iteration of the # events and have ensured nothing has set an error message diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 96d1d7f..e6a2e3b 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -237,6 +237,7 @@ def process_events(self): # Process other events possible_success = False for event in self.triggered_events: + # TODO make this more readable # Check events if ( event["event"]["module_id"] == "System" @@ -331,7 +332,9 @@ def process_events(self): event["event"]["module_id"] == "Balances" and event["event"]["event_id"] == "Deposit" ): - self.__total_fee_amount += event.value["attributes"]["amount"] + self.__total_fee_amount += event["event"]["attributes"][ + "amount" + ] if possible_success is True and self.__error_message is None: # we delay the positive setting of the __is_success flag until we have finished iteration of the # events and have ensured nothing has set an error message