Skip to content
Closed
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_cli/src/commands/crowd/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from bittensor_cli.src.bittensor.balances import Balance
from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
from bittensor_cli.src.commands.crowd.utils import (
get_effective_actor_ss58,
get_constant,
prompt_custom_call_params,
)
Expand Down Expand Up @@ -586,7 +587,8 @@ async def finalize_crowdloan(
print_error(error_msg)
return False, error_msg

if wallet.coldkeypub.ss58_address != crowdloan.creator:
creator_address = get_effective_actor_ss58(wallet=wallet, proxy=proxy)
if creator_address != crowdloan.creator:
error_msg = (
f"Only the creator can finalize a crowdloan. Creator: {crowdloan.creator}"
)
Expand Down
3 changes: 2 additions & 1 deletion bittensor_cli/src/commands/crowd/dissolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from bittensor_cli.src import COLORS
from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
from bittensor_cli.src.commands.crowd.utils import get_effective_actor_ss58
from bittensor_cli.src.commands.crowd.view import show_crowdloan_details
from bittensor_cli.src.bittensor.utils import (
blocks_to_duration,
Expand Down Expand Up @@ -50,7 +51,7 @@ async def dissolve_crowdloan(
tuple[bool, str]: Success status and message.
"""

creator_ss58 = wallet.coldkeypub.ss58_address
creator_ss58 = get_effective_actor_ss58(wallet=wallet, proxy=proxy)

crowdloan, current_block = await asyncio.gather(
subtensor.get_single_crowdloan(crowdloan_id),
Expand Down
7 changes: 5 additions & 2 deletions bittensor_cli/src/commands/crowd/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
print_extrinsic_id,
)
from bittensor_cli.src.commands.crowd.view import show_crowdloan_details
from bittensor_cli.src.commands.crowd.utils import get_constant
from bittensor_cli.src.commands.crowd.utils import (
get_constant,
get_effective_actor_ss58,
)


async def update_crowdloan(
Expand Down Expand Up @@ -88,7 +91,7 @@ async def update_crowdloan(
print_error(f"[red]{error_msg}[/red]")
return False, f"Crowdloan #{crowdloan_id} is already finalized."

creator_address = wallet.coldkeypub.ss58_address
creator_address = get_effective_actor_ss58(wallet=wallet, proxy=proxy)
if creator_address != crowdloan.creator:
error_msg = "Only the creator can update this crowdloan."
if json_output:
Expand Down
6 changes: 6 additions & 0 deletions bittensor_cli/src/commands/crowd/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
from typing import Optional

from async_substrate_interface.types import Runtime
from bittensor_wallet import Wallet
from rich.prompt import Prompt

from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
from bittensor_cli.src.bittensor.utils import console, json_console, print_error


def get_effective_actor_ss58(wallet: Wallet, proxy: Optional[str]) -> str:
"""Return the account address whose permissions apply for this call."""
return proxy or wallet.coldkeypub.ss58_address


async def prompt_custom_call_params(
subtensor: SubtensorInterface,
json_output: bool = False,
Expand Down
148 changes: 148 additions & 0 deletions tests/unit_tests/test_crowd_proxy_creator_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from bittensor_cli.src.bittensor.balances import Balance
from tests.unit_tests.conftest import COLDKEY_SS58, PROXY_SS58


def _make_crowdloan(
creator: str,
*,
finalized: bool = False,
raised_tao: float = 5.0,
cap_tao: float = 10.0,
) -> MagicMock:
crowdloan = MagicMock()
crowdloan.creator = creator
crowdloan.finalized = finalized
crowdloan.raised = Balance.from_tao(raised_tao)
crowdloan.cap = Balance.from_tao(cap_tao)
return crowdloan


def test_get_effective_actor_ss58_prefers_proxy(mock_wallet):
from bittensor_cli.src.commands.crowd.utils import get_effective_actor_ss58

assert get_effective_actor_ss58(wallet=mock_wallet, proxy=PROXY_SS58) == PROXY_SS58


def test_get_effective_actor_ss58_uses_wallet_when_proxy_missing(mock_wallet):
from bittensor_cli.src.commands.crowd.utils import get_effective_actor_ss58

assert (
get_effective_actor_ss58(wallet=mock_wallet, proxy=None)
== mock_wallet.coldkeypub.ss58_address
)


@pytest.mark.asyncio
async def test_finalize_crowdloan_allows_proxy_creator_actor(
mock_wallet, mock_subtensor
):
from bittensor_cli.src.commands.crowd.create import finalize_crowdloan

mock_subtensor.get_single_crowdloan = AsyncMock(
return_value=_make_crowdloan(creator=PROXY_SS58)
)
mock_subtensor.substrate.get_block_number = AsyncMock(return_value=12345)

result = await finalize_crowdloan(
subtensor=mock_subtensor,
wallet=mock_wallet,
proxy=PROXY_SS58,
crowdloan_id=7,
wait_for_inclusion=True,
wait_for_finalization=False,
prompt=False,
json_output=False,
)

assert result == (False, "Crowdloan has not reached its cap.")


@pytest.mark.asyncio
async def test_finalize_crowdloan_rejects_non_creator_proxy_actor(
mock_wallet, mock_subtensor
):
from bittensor_cli.src.commands.crowd.create import finalize_crowdloan

mock_subtensor.get_single_crowdloan = AsyncMock(
return_value=_make_crowdloan(creator=COLDKEY_SS58)
)
mock_subtensor.substrate.get_block_number = AsyncMock(return_value=12345)

result = await finalize_crowdloan(
subtensor=mock_subtensor,
wallet=mock_wallet,
proxy=PROXY_SS58,
crowdloan_id=7,
wait_for_inclusion=True,
wait_for_finalization=False,
prompt=False,
json_output=False,
)

assert result == (False, "Only the creator can finalize a crowdloan.")


@pytest.mark.asyncio
async def test_update_crowdloan_allows_proxy_creator_actor(mock_wallet, mock_subtensor):
from bittensor_cli.src.commands.crowd.update import update_crowdloan

mock_subtensor.get_single_crowdloan = AsyncMock(
return_value=_make_crowdloan(creator=PROXY_SS58)
)
mock_subtensor.substrate.get_chain_head = AsyncMock(return_value="0xhead")
mock_subtensor.substrate.get_block_number = AsyncMock(return_value=12345)
mock_subtensor.substrate.init_runtime = AsyncMock(return_value=MagicMock())

with (
patch(
"bittensor_cli.src.commands.crowd.update.get_constant",
new_callable=AsyncMock,
side_effect=[Balance.from_tao(1).rao, 1, 1000],
),
patch(
"bittensor_cli.src.commands.crowd.update.show_crowdloan_details",
new_callable=AsyncMock,
),
):
result = await update_crowdloan(
subtensor=mock_subtensor,
wallet=mock_wallet,
proxy=PROXY_SS58,
crowdloan_id=9,
min_contribution=None,
end=None,
cap=None,
prompt=False,
json_output=False,
)

assert result == (False, "No update parameter specified.")


@pytest.mark.asyncio
async def test_dissolve_crowdloan_allows_proxy_creator_actor(
mock_wallet, mock_subtensor
):
from bittensor_cli.src.commands.crowd.dissolve import dissolve_crowdloan

crowdloan = _make_crowdloan(creator=PROXY_SS58, raised_tao=12.0, cap_tao=20.0)
mock_subtensor.get_single_crowdloan = AsyncMock(return_value=crowdloan)
mock_subtensor.substrate.get_block_number = AsyncMock(return_value=12345)
mock_subtensor.get_crowdloan_contribution = AsyncMock(
return_value=Balance.from_tao(1.0)
)

result = await dissolve_crowdloan(
subtensor=mock_subtensor,
wallet=mock_wallet,
proxy=PROXY_SS58,
crowdloan_id=11,
prompt=False,
json_output=False,
)

assert result == (False, "Crowdloan not ready to dissolve.")
Loading