diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 16e68c254..3c2f38718 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8839,6 +8839,22 @@ def crowd_create( help="Block number when subnet lease ends (omit for perpetual lease).", min=1, ), + call_module: Optional[str] = typer.Option( + None, + "--call-module", + help="Custom call pallet/module name to attach to the crowdloan.", + ), + call_function: Optional[str] = typer.Option( + None, + "--call-function", + "--call-method", + help="Custom call function/method name to attach to the crowdloan.", + ), + call_args: Optional[str] = typer.Option( + None, + "--call-args", + help="JSON object of call arguments for the custom call (e.g. '{\"netuid\": 1}').", + ), prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8886,6 +8902,9 @@ def crowd_create( subnet_lease=subnet_lease, emissions_share=emissions_share, lease_end_block=lease_end_block, + call_module=call_module, + call_function=call_function, + call_args=call_args, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index bf2f91a23..cdc0fbcf7 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1925,6 +1925,44 @@ 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, + reuse_block: bool = False, + ) -> list[tuple[str, Balance]]: + """ + Retrieves all contributors and their contributions for a specific crowdloan. + + Args: + crowdloan_id: The ID of the crowdloan. + block_hash: Optional block hash to query against. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + List of (contributor_ss58, contribution_balance) tuples. + """ + result = await self.substrate.query_map( + module="Crowdloan", + storage_function="Contributions", + params=[crowdloan_id], + block_hash=block_hash, + reuse_block_hash=reuse_block, + fully_exhaust=True, + ) + contributors: list[tuple[str, Balance]] = [] + async for key, value in result: + # For double-map storage, the contributor is the remaining key after crowdloan_id. + contributor_key = key[0] if isinstance(key, (list, tuple)) else key + contributor_ss58 = decode_account_id(contributor_key) + amount = ( + Balance.from_rao(value.value) + if hasattr(value, "value") + else Balance.from_rao(value) + ) + contributors.append((contributor_ss58, amount)) + return contributors + async def get_coldkey_swap_schedule_duration( self, block_hash: Optional[str] = None, diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index ff64e41a0..ad0e793c2 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -36,6 +36,9 @@ async def create_crowdloan( subnet_lease: Optional[bool], emissions_share: Optional[int], lease_end_block: Optional[int], + call_module: Optional[str], + call_function: Optional[str], + call_args: Optional[str], wait_for_inclusion: bool, wait_for_finalization: bool, prompt: bool, @@ -273,6 +276,51 @@ async def create_crowdloan( call_to_attach = None + if call_module or call_function or call_args: + if not call_module or not call_function: + error_msg = "Both --call-module and --call-function are required for custom calls." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") + return False, error_msg + if crowdloan_type == "subnet": + error_msg = "Custom calls cannot be combined with subnet lease crowdloans." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") + return False, error_msg + try: + call_params = json.loads(call_args) if call_args else {} + except json.JSONDecodeError as exc: + error_msg = f"Invalid JSON for --call-args: {exc}" + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") + return False, error_msg + if call_params and not isinstance(call_params, dict): + error_msg = "--call-args must be a JSON object." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") + return False, error_msg + try: + call_to_attach = await subtensor.substrate.compose_call( + call_module=call_module, + call_function=call_function, + call_params=call_params, + ) + except Exception as exc: + error_msg = f"Failed to compose custom call: {exc}" + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") + return False, error_msg + creator_balance = await subtensor.get_balance( proxy or wallet.coldkeypub.ss58_address ) @@ -336,6 +384,8 @@ async def create_crowdloan( else f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]" ) table.add_row("Target address", target_text) + if call_module and call_function: + table.add_row("Custom Call", f"{call_module}.{call_function}") table.add_row("Deposit", f"[{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]") table.add_row( @@ -408,6 +458,12 @@ async def create_crowdloan( output_dict["data"]["perpetual_lease"] = lease_end_block is None else: output_dict["data"]["target_address"] = target_address + if call_module and call_function: + output_dict["data"]["call"] = { + "pallet": call_module, + "method": call_function, + "args": json.loads(call_args) if call_args else {}, + } json_console.print(json.dumps(output_dict)) message = f"{crowdloan_type.capitalize()} crowdloan created successfully." diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index 9a248d18f..d0a2f8da7 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -25,6 +25,11 @@ def _shorten(account: Optional[str]) -> str: return f"{account[:6]}…{account[-6:]}" +def _identity_label(account: str, identities: dict[str, dict]) -> str: + identity = identities.get(account, {}) + return identity.get("name") or identity.get("display") or _shorten(account) + + def _status(loan: CrowdloanData, current_block: int) -> str: if loan.finalized: return "Finalized" @@ -358,6 +363,31 @@ async def show_crowdloan_details( } status_color = status_color_map.get(status, "white") + contributors: list[tuple[str, Balance]] = [] + contributors_view: list[dict[str, object]] = [] + if crowdloan.contributors_count > 0: + status_msg = "Fetching contributors and identities..." if not json_output else None + status_ctx = console.status(status_msg) if status_msg else None + try: + if status_ctx: + status_ctx.__enter__() + contributors, identities = await asyncio.gather( + subtensor.get_crowdloan_contributors(crowdloan_id), + subtensor.query_all_identities(), + ) + contributors_view = [ + { + "address": address, + "identity": _identity_label(address, identities), + "amount": contribution.tao, + } + for address, contribution in contributors + ] + contributors_view.sort(key=lambda x: x["amount"], reverse=True) + finally: + if status_ctx: + status_ctx.__exit__(None, None, None) + if json_output: time_remaining = _time_remaining(crowdloan, current_block) @@ -429,6 +459,7 @@ async def show_crowdloan_details( "current_block": current_block, "time_remaining": time_remaining, "contributors_count": crowdloan.contributors_count, + "contributors": contributors_view, "average_contribution": avg_contribution, "target_address": crowdloan.target_address, "has_call": crowdloan.has_call, @@ -638,4 +669,27 @@ async def show_crowdloan_details( table.add_row(arg_name, str(display_value)) console.print(table) + if contributors_view: + contrib_table = Table( + Column("Contributor", style=COLORS.G.SUBHEAD, no_wrap=True), + Column("Identity", style=COLORS.G.TEMPO), + Column(f"Contribution ({Balance.get_unit(0)})", style=COLORS.P.TAO), + title=f"[{COLORS.G.HEADER}]Contributors[/]", + show_header=True, + show_edge=False, + box=box.SIMPLE, + border_style="bright_black", + ) + for entry in contributors_view: + amount = ( + f"τ {entry['amount']:,.4f}" + if verbose + else f"τ {millify_tao(entry['amount'])}" + ) + contrib_table.add_row( + _shorten(entry["address"]), + entry["identity"], + amount, + ) + console.print(contrib_table) return True, f"Displayed info for crowdloan #{crowdloan_id}"