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
126 changes: 124 additions & 2 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
view as view_crowdloan,
update as crowd_update,
refund as crowd_refund,
contributors as crowd_contributors,
)
from bittensor_cli.src.commands.liquidity.utils import (
prompt_liquidity,
Expand Down Expand Up @@ -1334,6 +1335,9 @@ def __init__(self):
self.crowd_app.command("info", rich_help_panel=HELP_PANELS["CROWD"]["INFO"])(
self.crowd_info
)
self.crowd_app.command(
"contributors", rich_help_panel=HELP_PANELS["CROWD"]["INFO"]
)(self.crowd_contributors)
self.crowd_app.command(
"create", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"]
)(self.crowd_create)
Expand Down Expand Up @@ -2904,14 +2908,15 @@ def wallet_inspect(
),
wallet_name: str = Options.wallet_name,
wallet_path: str = Options.wallet_path,
wallet_hotkey: str = Options.wallet_hotkey,
network: Optional[list[str]] = Options.network,
netuids: str = Options.netuids,
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
):
"""
Displays the details of the user's wallet (coldkey) on the Bittensor network.
Displays the details of the user's wallet pairs (coldkey, hotkey) on the Bittensor network.

The output is presented as a table with the below columns:

Expand Down Expand Up @@ -2956,7 +2961,7 @@ def wallet_inspect(
ask_for = [WO.NAME, WO.PATH] if not all_wallets else [WO.PATH]
validate = WV.WALLET if not all_wallets else WV.NONE
wallet = self.wallet_ask(
wallet_name, wallet_path, None, ask_for=ask_for, validate=validate
wallet_name, wallet_path, wallet_hotkey, ask_for=ask_for, validate=validate
)

self.initialize_chain(network)
Expand Down Expand Up @@ -8745,6 +8750,31 @@ def crowd_list(
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
status: Optional[str] = typer.Option(
None,
"--status",
help="Filter by status: active, funded, closed, finalized",
),
type_filter: Optional[str] = typer.Option(
None,
"--type",
help="Filter by type: subnet, fundraising",
),
sort_by: Optional[str] = typer.Option(
None,
"--sort-by",
help="Sort by: raised, end, contributors, id",
),
sort_order: Optional[str] = typer.Option(
None,
"--sort-order",
help="Sort order: asc, desc (default: desc for raised, asc for id)",
),
search_creator: Optional[str] = typer.Option(
None,
"--search-creator",
help="Search by creator address or identity name",
),
):
"""
List crowdloans together with their funding progress and key metadata.
Expand All @@ -8754,19 +8784,34 @@ def crowd_list(
or a general fundraising crowdloan.

Use `--verbose` for full-precision amounts and longer addresses.
Use `--status` to filter by status (active, funded, closed, finalized).
Use `--type` to filter by type (subnet, fundraising).
Use `--sort-by` and `--sort-order` to sort results.
Use `--search-creator` to search by creator address or identity name.

EXAMPLES

[green]$[/green] btcli crowd list

[green]$[/green] btcli crowd list --verbose

[green]$[/green] btcli crowd list --status active --type subnet

[green]$[/green] btcli crowd list --sort-by raised --sort-order desc

[green]$[/green] btcli crowd list --search-creator "5D..."
"""
self.verbosity_handler(quiet, verbose, json_output, prompt=False)
return self._run_command(
view_crowdloan.list_crowdloans(
subtensor=self.initialize_chain(network),
verbose=verbose,
json_output=json_output,
status_filter=status,
type_filter=type_filter,
sort_by=sort_by,
sort_order=sort_order,
search_creator=search_creator,
)
)

Expand All @@ -8786,17 +8831,25 @@ def crowd_info(
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
show_contributors: bool = typer.Option(
False,
"--show-contributors",
help="Show contributor list with identities.",
),
):
"""
Display detailed information about a specific crowdloan.

Includes funding progress, target account, and call details among other information.
Use `--show-contributors` to display the list of contributors (default: false).

EXAMPLES

[green]$[/green] btcli crowd info --id 0

[green]$[/green] btcli crowd info --id 1 --verbose

[green]$[/green] btcli crowd info --id 0 --show-contributors true
"""
self.verbosity_handler(quiet, verbose, json_output, prompt=False)

Expand Down Expand Up @@ -8824,6 +8877,53 @@ def crowd_info(
wallet=wallet,
verbose=verbose,
json_output=json_output,
show_contributors=show_contributors,
)
)

def crowd_contributors(
self,
crowdloan_id: Optional[int] = typer.Option(
None,
"--crowdloan-id",
"--crowdloan_id",
"--id",
help="The ID of the crowdloan to list contributors for",
),
network: Optional[list[str]] = Options.network,
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
):
"""
List all contributors to a specific crowdloan.

Shows contributor addresses, contribution amounts, identity names, and percentages.
Contributors are sorted by contribution amount (highest first).

EXAMPLES

[green]$[/green] btcli crowd contributors --id 0

[green]$[/green] btcli crowd contributors --id 1 --verbose

[green]$[/green] btcli crowd contributors --id 2 --json-output
"""
self.verbosity_handler(quiet, verbose, json_output, prompt=False)

if crowdloan_id is None:
crowdloan_id = IntPrompt.ask(
f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]",
default=None,
show_default=False,
)

return self._run_command(
crowd_contributors.list_contributors(
subtensor=self.initialize_chain(network),
crowdloan_id=crowdloan_id,
verbose=verbose,
json_output=json_output,
)
)

Expand Down Expand Up @@ -8886,6 +8986,21 @@ def crowd_create(
help="Block number when subnet lease ends (omit for perpetual lease).",
min=1,
),
custom_call_pallet: Optional[str] = typer.Option(
None,
"--custom-call-pallet",
help="Pallet name for custom Substrate call to attach to crowdloan.",
),
custom_call_method: Optional[str] = typer.Option(
None,
"--custom-call-method",
help="Method name for custom Substrate call to attach to crowdloan.",
),
custom_call_args: Optional[str] = typer.Option(
None,
"--custom-call-args",
help='JSON string of arguments for custom call (e.g., \'{"arg1": "value1", "arg2": 123}\').',
),
prompt: bool = Options.prompt,
wait_for_inclusion: bool = Options.wait_for_inclusion,
wait_for_finalization: bool = Options.wait_for_finalization,
Expand All @@ -8898,6 +9013,7 @@ def crowd_create(
Create a crowdloan that can either:
1. Raise funds for a specific address (general fundraising)
2. Create a new leased subnet where contributors receive emissions
3. Attach any custom Substrate call (using --custom-call-pallet, --custom-call-method, --custom-call-args)

EXAMPLES

Expand All @@ -8909,6 +9025,9 @@ def crowd_create(

Subnet lease ending at block 500000:
[green]$[/green] btcli crowd create --subnet-lease --emissions-share 25 --lease-end-block 500000

Custom call:
[green]$[/green] btcli crowd create --deposit 10 --cap 1000 --duration 1000 --min-contribution 1 --custom-call-pallet "SomeModule" --custom-call-method "some_method" --custom-call-args '{"param1": "value", "param2": 42}'
"""
self.verbosity_handler(quiet, verbose, json_output, prompt)
proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only)
Expand All @@ -8933,6 +9052,9 @@ def crowd_create(
subnet_lease=subnet_lease,
emissions_share=emissions_share,
lease_end_block=lease_end_block,
custom_call_pallet=custom_call_pallet,
custom_call_method=custom_call_method,
custom_call_args=custom_call_args,
wait_for_inclusion=wait_for_inclusion,
wait_for_finalization=wait_for_finalization,
prompt=prompt,
Expand Down
67 changes: 67 additions & 0 deletions bittensor_cli/src/bittensor/subtensor_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1925,6 +1925,43 @@ async def get_crowdloan_contribution(
return Balance.from_rao(contribution)
return None

async def get_crowdloan_contributors(
self,
crowdloan_id: int,
block_hash: Optional[str] = None,
) -> dict[str, Balance]:
"""Retrieves all contributors and their contributions for a specific crowdloan.

Args:
crowdloan_id (int): The ID of the crowdloan.
block_hash (Optional[str]): The blockchain block hash at which to perform the query.

Returns:
dict[str, Balance]: A dictionary mapping contributor SS58 addresses to their
contribution amounts as Balance objects.

This function queries the Contributions storage map with the crowdloan_id as the first key
to retrieve all contributors and their contribution amounts.
"""
contributors_data = await self.substrate.query_map(
module="Crowdloan",
storage_function="Contributions",
params=[crowdloan_id],
block_hash=block_hash,
fully_exhaust=True,
)

contributor_contributions = {}
async for contributor_key, contribution_amount in contributors_data:
try:
contributor_address = decode_account_id(contributor_key[0])
contribution_balance = Balance.from_rao(contribution_amount.value)
contributor_contributions[contributor_address] = contribution_balance
except Exception:
continue

return contributor_contributions

async def get_coldkey_swap_schedule_duration(
self,
block_hash: Optional[str] = None,
Expand Down Expand Up @@ -2501,6 +2538,36 @@ async def get_mev_shield_current_key(

return public_key_bytes

async def compose_custom_crowdloan_call(
self,
pallet_name: str,
method_name: str,
call_params: dict,
block_hash: Optional[str] = None,
) -> tuple[Optional[GenericCall], Optional[str]]:
"""
Compose a custom Substrate call.

Args:
pallet_name: Name of the pallet/module
method_name: Name of the method/function
call_params: Dictionary of call parameters
block_hash: Optional block hash for the query

Returns:
Tuple of (GenericCall or None, error_message or None)
"""
try:
call = await self.substrate.compose_call(
call_module=pallet_name,
call_function=method_name,
call_params=call_params,
block_hash=block_hash,
)
return call, None
except Exception as e:
return None, f"Failed to compose call: {str(e)}"


async def best_connection(networks: list[str]):
"""
Expand Down
Loading