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
4 changes: 3 additions & 1 deletion bittensor/core/async_subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
50 changes: 33 additions & 17 deletions bittensor/core/extrinsics/asyncex/proxy.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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]")

Expand Down
47 changes: 30 additions & 17 deletions bittensor/core/extrinsics/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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]")

Expand Down
62 changes: 62 additions & 0 deletions bittensor/core/extrinsics/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
4 changes: 3 additions & 1 deletion bittensor/core/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
4 changes: 1 addition & 3 deletions bittensor/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions tests/e2e_tests/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/extrinsics/asyncex/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/extrinsics/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down