Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .github/workflows/check-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
95 changes: 79 additions & 16 deletions async_substrate_interface/async_substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,15 @@ async def process_events(self):
has_transaction_fee_paid_event = True

# 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"
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"][
Expand All @@ -294,13 +295,26 @@ 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"
):
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):
Expand Down Expand Up @@ -365,7 +379,13 @@ 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
self.__is_success = True

@property
async def is_success(self) -> bool:
Expand Down Expand Up @@ -833,7 +853,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):
"""
Expand Down Expand Up @@ -891,12 +911,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
Expand Down Expand Up @@ -939,7 +969,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)
Expand Down Expand Up @@ -3987,6 +4024,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:
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)
logger.error(failure_message)
raise SubstrateRequestException(failure_message)

if "finalized" in message_result and wait_for_finalization:
logger.debug("Extrinsic finalized. Unsubscribing.")
Expand All @@ -3998,7 +4061,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
):
Expand Down
60 changes: 53 additions & 7 deletions async_substrate_interface/sync_substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,15 @@ def process_events(self):
has_transaction_fee_paid_event = True

# 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"
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"][
Expand All @@ -255,13 +256,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):
Expand Down Expand Up @@ -318,7 +332,13 @@ 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
self.__is_success = True

@property
def is_success(self) -> bool:
Expand Down Expand Up @@ -3170,6 +3190,32 @@ 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:
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:
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
# TODO change this logic
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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" }
Expand Down
Loading