diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index a307efa79e..9d0fd2a5cc 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -5445,7 +5445,7 @@ async def sign_and_send_extrinsic( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - extrinsic_response.extrinsic_receipt = response + # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: extrinsic_response.extrinsic_fee = await self.get_extrinsic_fee( @@ -5457,6 +5457,8 @@ async def sign_and_send_extrinsic( logging.debug(extrinsic_response.message) return extrinsic_response + extrinsic_response.extrinsic_receipt = response + if await response.is_success: extrinsic_response.extrinsic_fee = Balance.from_rao( await response.total_fee_amount diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py index 07b8d3890b..acf764d0b8 100644 --- a/bittensor/core/extrinsics/asyncex/proxy.py +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -1,7 +1,9 @@ +import asyncio from typing import TYPE_CHECKING, Optional, Union from bittensor.core.chain_data.proxy import ProxyType from bittensor.core.extrinsics.pallets import Proxy +from bittensor.core.extrinsics.utils import apply_pure_proxy_data from bittensor.core.types import ExtrinsicResponse from bittensor.utils.btlogging import logging @@ -42,6 +44,13 @@ async def add_proxy_extrinsic( Returns: ExtrinsicResponse: The result object of the extrinsic execution. + + Warning: + If ``wait_for_inclusion=False`` or when ``block_hash`` is not available, the extrinsic receipt may not contain + triggered events. This means that any data that would normally be extracted from blockchain events (such as + proxy relationship details) will not be available in the response. To ensure complete event data is available, + either pass ``wait_for_inclusion=True`` when calling this function, or retrieve the data manually from the + blockchain using the extrinsic hash. """ try: if not ( @@ -273,23 +282,30 @@ async def create_pure_proxy_extrinsic( if response.success: logging.debug("[green]Pure proxy created successfully.[/green]") - # Extract pure proxy address from PureCreated triggered event - for event in await response.extrinsic_receipt.triggered_events: - if event.get("event_id") == "PureCreated": - # Event structure: PureProxyCreated { disambiguation_index, proxy_type, pure, who } - attributes = event.get("attributes", []) - if attributes: - response.data = { - "pure_account": attributes.get("pure"), - "spawner": attributes.get("who"), - "proxy_type": attributes.get("proxy_type"), - "index": attributes.get("disambiguation_index"), - "height": await subtensor.substrate.get_block_number( - response.extrinsic_receipt.block_hash - ), - "ext_index": await response.extrinsic_receipt.extrinsic_idx, - } - break + block_hash = ( + response.extrinsic_receipt.block_hash + if response.extrinsic_receipt + else None + ) + + block_number, triggered_events, extrinsic_idx = ( + await asyncio.gather( + subtensor.substrate.get_block_number(block_hash=block_hash), + response.extrinsic_receipt.triggered_events, + response.extrinsic_receipt.extrinsic_idx, + ) + if (wait_for_finalization or wait_for_inclusion) + and response.extrinsic_receipt.block_hash + else (0, [], 0) + ) + + response = apply_pure_proxy_data( + response=response, + triggered_events=triggered_events, + block_number=block_number, + extrinsic_idx=extrinsic_idx, + raise_error=raise_error, + ) else: logging.error(f"[red]{response.message}[/red]") diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index e78a932dcc..1942201af6 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -2,6 +2,7 @@ from bittensor.core.chain_data.proxy import ProxyType from bittensor.core.extrinsics.pallets import Proxy +from bittensor.core.extrinsics.utils import apply_pure_proxy_data from bittensor.core.types import ExtrinsicResponse from bittensor.utils.btlogging import logging @@ -239,6 +240,13 @@ def create_pure_proxy_extrinsic( Returns: ExtrinsicResponse: The result object of the extrinsic execution. + + Warning: + If ``wait_for_inclusion=False`` or when ``block_hash`` is not available, the extrinsic receipt may not contain + triggered events. This means that any data that would normally be extracted from blockchain events (such as + proxy relationship details) will not be available in the response. To ensure complete event data is available, + either pass ``wait_for_inclusion=True`` when calling this function, or retrieve the data manually from the + blockchain using the extrinsic hash. """ try: if not ( @@ -272,23 +280,28 @@ def create_pure_proxy_extrinsic( if response.success: logging.debug("[green]Pure proxy created successfully.[/green]") - # Extract pure proxy address from PureCreated triggered event - for event in response.extrinsic_receipt.triggered_events: - if event.get("event_id") == "PureCreated": - # Event structure: PureProxyCreated { disambiguation_index, proxy_type, pure, who } - attributes = event.get("attributes", []) - if attributes: - response.data = { - "pure_account": attributes.get("pure"), - "spawner": attributes.get("who"), - "proxy_type": attributes.get("proxy_type"), - "index": attributes.get("disambiguation_index"), - "height": subtensor.substrate.get_block_number( - response.extrinsic_receipt.block_hash - ), - "ext_index": response.extrinsic_receipt.extrinsic_idx, - } - break + block_number, triggered_events, extrinsic_idx = ( + ( + subtensor.substrate.get_block_number( + block_hash=response.extrinsic_receipt.block_hash + if response.extrinsic_receipt + else None + ), + response.extrinsic_receipt.triggered_events, + response.extrinsic_receipt.extrinsic_idx, + ) + if (wait_for_finalization or wait_for_inclusion) + and response.extrinsic_receipt.block_hash + else (0, [], 0) + ) + + response = apply_pure_proxy_data( + response=response, + triggered_events=triggered_events, + block_number=block_number, + extrinsic_idx=extrinsic_idx, + raise_error=raise_error, + ) else: logging.error(f"[red]{response.message}[/red]") diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index d23c314f06..983c8cbc12 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -145,3 +145,65 @@ def sudo_call_extrinsic( except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def apply_pure_proxy_data( + response: ExtrinsicResponse, + triggered_events: list, + block_number: int, + extrinsic_idx: int, + raise_error: bool, +) -> ExtrinsicResponse: + """Apply pure proxy data to the response object. + + Parameters: + response: The response object to update. + triggered_events: The triggered events of the transaction. + block_number: The block number of the transaction. + extrinsic_idx: The index of the extrinsic in the transaction. + raise_error: Whether to raise an error if the data cannot be applied successfully. + + Returns: + True if the data was applied successfully, False otherwise. + """ + # Extract pure proxy address from PureCreated triggered event if wait_for_inclusion is True. + for event in triggered_events: + if event.get("event_id") == "PureCreated": + # Event structure: PureProxyCreated { disambiguation_index, proxy_type, pure, who } + attributes = event.get("attributes", []) + if attributes: + response.data = { + "pure_account": attributes.get("pure"), + "spawner": attributes.get("who"), + "proxy_type": attributes.get("proxy_type"), + "index": attributes.get("disambiguation_index"), + "height": block_number, + "ext_index": extrinsic_idx, + } + return response + + # If triggered events are not available or event PureCreated does not exist in the response, return the response + # with warning message ot raise the error if raise_error is True. + message = ( + f"The ExtrinsicResponse doesn't contain pure_proxy data (`pure_account`, `spawner`, `proxy_type`, etc.) " + f"because the extrinsic receipt doesn't have triggered events. This typically happens when " + f"`wait_for_inclusion=False` or when `block_hash` is not available. To get this data, either pass " + f"`wait_for_inclusion=True` when calling this function, or retrieve the data manually from the blockchain " + f"using the extrinsic hash." + ) + if response.extrinsic is not None and hasattr(response.extrinsic, "extrinsic_hash"): + extrinsic_hash = response.extrinsic.extrinsic_hash + hash_str = ( + extrinsic_hash.hex() + if hasattr(extrinsic_hash, "hex") + else str(extrinsic_hash) + ) + message += f" Extrinsic hash: `{hash_str}`" + + response.success = False + response.message = message + + if raise_error: + raise RuntimeError(message) + + return response.with_log("warning") diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index ac8babe820..8b279e4974 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4203,7 +4203,7 @@ def sign_and_send_extrinsic( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - extrinsic_response.extrinsic_receipt = response + # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: extrinsic_response.extrinsic_fee = self.get_extrinsic_fee( @@ -4215,6 +4215,8 @@ def sign_and_send_extrinsic( logging.debug(extrinsic_response.message) return extrinsic_response + extrinsic_response.extrinsic_receipt = response + if response.is_success: extrinsic_response.extrinsic_fee = Balance.from_rao( response.total_fee_amount diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 9d10e07850..af1e7dd2bc 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -352,9 +352,7 @@ class ExtrinsicResponse: extrinsic_function: Optional[str] = None extrinsic: Optional["GenericExtrinsic"] = None extrinsic_fee: Optional["Balance"] = None - extrinsic_receipt: Optional[Union["ExtrinsicReceipt", "AsyncExtrinsicReceipt"]] = ( - None - ) + extrinsic_receipt: Optional["AsyncExtrinsicReceipt | ExtrinsicReceipt"] = None transaction_tao_fee: Optional["Balance"] = None transaction_alpha_fee: Optional["Balance"] = None error: Optional[Exception] = None diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index 82e67d5946..9e01d39224 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -1004,6 +1004,20 @@ def test_create_and_kill_pure_proxy(subtensor, alice_wallet, bob_wallet): delay = 0 index = 0 + # === Tests failed Create pure proxy without wait_for*=True === + response = subtensor.proxies.create_pure_proxy( + wallet=spawner_wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + assert not response.success + assert "The ExtrinsicResponse doesn't contain pure_proxy data" in response.message + + subtensor.wait_for_block() + # === Create pure proxy === response = subtensor.proxies.create_pure_proxy( wallet=spawner_wallet, @@ -1133,6 +1147,20 @@ async def test_create_and_kill_pure_proxy_async( delay = 0 index = 0 + # === Tests failed Create pure proxy without wait_for*=True === + response = await async_subtensor.proxies.create_pure_proxy( + wallet=spawner_wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + assert not response.success + assert "The ExtrinsicResponse doesn't contain pure_proxy data" in response.message + + await async_subtensor.wait_for_block() + # === Create pure proxy === response = await async_subtensor.proxies.create_pure_proxy( wallet=spawner_wallet, diff --git a/tests/unit_tests/extrinsics/asyncex/test_proxy.py b/tests/unit_tests/extrinsics/asyncex/test_proxy.py index 0f3df0caf7..fd79ca3615 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_proxy.py +++ b/tests/unit_tests/extrinsics/asyncex/test_proxy.py @@ -160,7 +160,7 @@ async def test_create_pure_proxy_extrinsic(subtensor, mocker): } ] )() - mock_response.extrinsic_receipt.block_hash = mocker.AsyncMock(spec=str) + mock_response.extrinsic_receipt.block_hash = "NOT NONE BLOCK HASH" mock_response.extrinsic_receipt.extrinsic_idx = mocker.AsyncMock(return_value=1)() mocked_sign_and_send_extrinsic.return_value = mock_response diff --git a/tests/unit_tests/extrinsics/test_proxy.py b/tests/unit_tests/extrinsics/test_proxy.py index bea56fbbdb..5313538b45 100644 --- a/tests/unit_tests/extrinsics/test_proxy.py +++ b/tests/unit_tests/extrinsics/test_proxy.py @@ -145,7 +145,7 @@ def test_create_pure_proxy_extrinsic(subtensor, mocker): }, } ] - mock_response.extrinsic_receipt.block_hash = mocker.MagicMock(spec=str) + mock_response.extrinsic_receipt.block_hash = "NOT NONE BLOCK HASH" mock_response.extrinsic_receipt.extrinsic_idx = 1 mocked_sign_and_send_extrinsic.return_value = mock_response