From 449483939d799342da87f95bf15d8d79cbedd7ab Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 10 Oct 2025 14:53:40 -0700 Subject: [PATCH 01/69] add label text --- bittensor_cli/src/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 1151a2c4..9b7d749b 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -732,6 +732,11 @@ class RootSudoOnly(Enum): "LIQUIDITY": { "LIQUIDITY_MGMT": "Liquidity Management", }, + "CROWD": { + "INITIATOR": "Crowdloan Creation & Management", + "PARTICIPANT": "Crowdloan Participation", + "INFO": "Crowdloan Information", + }, } From 748a46436ee319b50eecece1bbda6ae032ee2dc3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 10 Oct 2025 14:55:13 -0700 Subject: [PATCH 02/69] add crowd_create --- bittensor_cli/cli.py | 89 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a73aa2e8..2c75e913 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -66,6 +66,7 @@ ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds +from bittensor_cli.src.commands.crowd import initiator as crowd_initiator from bittensor_cli.src.commands.liquidity import liquidity from bittensor_cli.src.commands.liquidity.utils import ( prompt_liquidity, @@ -678,6 +679,7 @@ class CLIManager: subnets_app: typer.Typer subnet_mechanisms_app: typer.Typer weights_app: typer.Typer + crowd_app: typer.Typer utils_app: typer.Typer view_app: typer.Typer asyncio_runner = asyncio @@ -755,6 +757,7 @@ def __init__(self): self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) self.liquidity_app = typer.Typer(epilog=_epilog) + self.crowd_app = typer.Typer(epilog=_epilog) self.utils_app = typer.Typer(epilog=_epilog) # config alias @@ -1132,6 +1135,18 @@ def __init__(self): self.sudo_app.command("get_take", hidden=True)(self.sudo_get_take) self.sudo_app.command("set_take", hidden=True)(self.sudo_set_take) + # Crowdloan + self.app.add_typer( + self.crowd_app, + name="crowd", + short_help="Crowdloan commands, aliases: `cr`, `crowdloan`", + no_args_is_help=True, + ) + self.app.add_typer(self.crowd_app, name="cr", hidden=True, no_args_is_help=True) + self.app.add_typer( + self.crowd_app, name="crowdloan", hidden=True, no_args_is_help=True + ) + # Liquidity self.app.add_typer( self.liquidity_app, @@ -1155,6 +1170,9 @@ def __init__(self): self.liquidity_app.command( "remove", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] )(self.liquidity_remove) + self.crowd_app.command( + "create", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] + )(self.crowd_create) # utils app self.utils_app.command("convert")(self.convert) @@ -7228,6 +7246,77 @@ def liquidity_modify( ) ) + def crowd_create( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + deposit: Optional[float] = typer.Option( + None, + "--deposit", + help="Initial deposit in TAO to secure the crowdloan.", + min=1, + ), + min_contribution: Optional[float] = typer.Option( + None, + "--min-contribution", + "--min_contribution", + help="Minimum contribution amount in TAO.", + min=0.1, + ), + cap: Optional[int] = typer.Option( + None, + "--cap", + help="Maximum amount in TAO the crowdloan will raise.", + min=1, + ), + duration: Optional[int] = typer.Option( + None, + "--duration", + help="Crowdloan duration in blocks.", + min=1, + ), + target_address: Optional[str] = typer.Option( + None, + "--target-address", + "--target", + help="Optional target SS58 address to receive the raised funds.", + ), + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """Create a new crowdloan.""" + self.verbosity_handler(quiet, verbose, json_output) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + return self._run_command( + crowd_initiator.create_crowdloan( + subtensor=self.initialize_chain(network), + wallet=wallet, + deposit_tao=deposit, + min_contribution_tao=min_contribution, + cap_tao=cap, + duration_blocks=duration, + target_address=target_address, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + json_output=json_output, + ) + ) + @staticmethod def convert( from_rao: Optional[str] = typer.Option( From a6954d32a1c2b493c4a4d4e8253ab89e9bcb826b Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 14:22:01 -0700 Subject: [PATCH 03/69] get_crowdloans --- .../src/bittensor/subtensor_interface.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index fe5e7af2..26869c58 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -30,6 +30,7 @@ SubnetState, MetagraphInfo, SimSwapResult, + CrowdloanData, ) from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float @@ -1693,6 +1694,27 @@ async def get_scheduled_coldkey_swap( keys_pending_swap.append(decode_account_id(ss58)) return keys_pending_swap + async def get_crowdloans( + self, block_hash: Optional[str] = None + ) -> list[CrowdloanData]: + """ + Retrieves all crowdloans stored in the `Crowdloan::Crowdloans` map. + + :param block_hash: Optional block hash to query against. + :return: A list of `CrowdloanData` objects describing each crowdloan. + """ + crowdloans_data = await self.substrate.query_map( + module="Crowdloan", + storage_function="Crowdloans", + block_hash=block_hash, + fully_exhaust=True, + ) + crowdloans = {} + async for fund_id, fund_info in crowdloans_data: + crowdloans[fund_id] = CrowdloanData.from_any(fund_info) + + return crowdloans + async def get_coldkey_swap_schedule_duration( self, block_hash: Optional[str] = None, From 4065dee67a05e46194611b6e9ab59e0c9b5f48ec Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 14:26:48 -0700 Subject: [PATCH 04/69] update docs --- bittensor_cli/src/bittensor/subtensor_interface.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 26869c58..4fd80eb4 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1697,11 +1697,16 @@ async def get_scheduled_coldkey_swap( async def get_crowdloans( self, block_hash: Optional[str] = None ) -> list[CrowdloanData]: - """ - Retrieves all crowdloans stored in the `Crowdloan::Crowdloans` map. + """Retrieves all crowdloans from the network. + + Args: + block_hash (Optional[str]): The blockchain block hash at which to perform the query. + + Returns: + dict[int, CrowdloanData]: A dictionary mapping crowdloan IDs to CrowdloanData objects + containing details such as creator, deposit, cap, raised amount, and finalization status. - :param block_hash: Optional block hash to query against. - :return: A list of `CrowdloanData` objects describing each crowdloan. + This function fetches information about all crowdloans """ crowdloans_data = await self.substrate.query_map( module="Crowdloan", From f1bbbead1dff3b241313d73b18e9c7cd1ca98e8b Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 14:27:48 -0700 Subject: [PATCH 05/69] CrowdloanData dataclass --- bittensor_cli/src/bittensor/chain_data.py | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index ffd4ba3f..e3ff542e 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1213,3 +1213,45 @@ def from_dict(cls, d: dict, netuid: int) -> "SimSwapResult": tao_fee=Balance.from_rao(d["tao_fee"]).set_unit(0), alpha_fee=Balance.from_rao(d["alpha_fee"]).set_unit(netuid), ) + + +@dataclass +class CrowdloanData(InfoBase): + creator: Optional[str] + funds_account: Optional[str] + deposit: Balance + min_contribution: Balance + cap: Balance + raised: Balance + end: int + finalized: bool + contributors_count: int + target_address: Optional[str] + has_call: bool + + @classmethod + def _fix_decoded(cls, decoded: dict[str, Any]) -> "CrowdloanData": + creator = decode_account_id(decoded["creator"]) if decoded["creator"] else None + funds_account = ( + decode_account_id(decoded["funds_account"]) + if decoded["funds_account"] + else None + ) + target_address = ( + decode_account_id(decoded["target_address"]) + if decoded["target_address"] + else None + ) + return cls( + creator=creator, + funds_account=funds_account, + deposit=Balance.from_rao(int(decoded["deposit"])), + min_contribution=Balance.from_rao(int(decoded["min_contribution"])), + cap=Balance.from_rao(int(decoded["cap"])), + raised=Balance.from_rao(int(decoded["raised"])), + end=int(decoded["end"]), + finalized=bool(decoded["finalized"]), + contributors_count=int(decoded["contributors_count"]), + target_address=target_address, + has_call=bool(decoded["call"]), + ) From 20dfa5285f1ccb5c19100b6161b9eabe9830a0ba Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 14:32:01 -0700 Subject: [PATCH 06/69] adds creation logic --- bittensor_cli/src/commands/crowd/__init__.py | 0 bittensor_cli/src/commands/crowd/create.py | 270 +++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 bittensor_cli/src/commands/crowd/__init__.py create mode 100644 bittensor_cli/src/commands/crowd/create.py diff --git a/bittensor_cli/src/commands/crowd/__init__.py b/bittensor_cli/src/commands/crowd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py new file mode 100644 index 00000000..8835b2b7 --- /dev/null +++ b/bittensor_cli/src/commands/crowd/create.py @@ -0,0 +1,270 @@ +import asyncio +from typing import Optional + +from bittensor_wallet import Wallet +from rich.prompt import Confirm, IntPrompt, Prompt, FloatPrompt + +from bittensor_cli.src import COLORS +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.utils import ( + blocks_to_duration, + console, + err_console, + is_valid_ss58_address, + print_extrinsic_id, + unlock_key, +) + + +async def create_crowdloan( + subtensor: SubtensorInterface, + wallet: Wallet, + deposit_tao: Optional[int], + min_contribution_tao: Optional[int], + cap_tao: Optional[int], + duration_blocks: Optional[int], + target_address: Optional[str], + wait_for_inclusion: bool, + wait_for_finalization: bool, + prompt: bool, + json_output: bool, +) -> tuple[bool, str]: + """ + Create a new crowdloan with the given parameters. + Prompts for missing parameters if not provided. + """ + + unlock_status = unlock_key(wallet) + if not unlock_status.success: + err_console.print(f"[red]{unlock_status.message}[/red]") + return False, unlock_status.message + + async def _get_constant(constant_name: str) -> int: + result = await subtensor.substrate.get_constant( + module_name="Crowdloan", + constant_name=constant_name, + ) + return getattr(result, "value", result) + + ( + minimum_deposit_raw, + min_contribution_raw, + min_duration, + max_duration, + ) = await asyncio.gather( + _get_constant("MinimumDeposit"), + _get_constant("AbsoluteMinimumContribution"), + _get_constant("MinimumBlockDuration"), + _get_constant("MaximumBlockDuration"), + ) + + minimum_deposit = Balance.from_rao(minimum_deposit_raw) + min_contribution = Balance.from_rao(min_contribution_raw) + + if not prompt: + missing_fields = [] + if deposit_tao is None: + missing_fields.append("--deposit") + if min_contribution_tao is None: + missing_fields.append("--min-contribution") + if cap_tao is None: + missing_fields.append("--cap") + if duration_blocks is None: + missing_fields.append("--duration") + if missing_fields: + err_console.print( + "[red]The following options must be provided when prompts are disabled:[/red] " + + ", ".join(missing_fields) + ) + return False, "Missing required options when prompts are disabled." + + deposit_value = deposit_tao + while True: + if deposit_value is None: + deposit_value = FloatPrompt.ask( + f"Enter the deposit amount in TAO " + f"[blue](>= {minimum_deposit.tao:,.4f})[/blue]" + ) + deposit = Balance.from_tao(deposit_value) + if deposit < minimum_deposit: + if prompt: + err_console.print( + f"[red]Deposit must be at least {minimum_deposit.tao:,.4f} TAO.[/red]" + ) + deposit_value = None + continue + err_console.print( + f"[red]Deposit is below the minimum required deposit " + f"({minimum_deposit.tao:,.4f} TAO).[/red]" + ) + return False, "Deposit is below the minimum required deposit." + break + + min_contribution_value = min_contribution_tao + while True: + if min_contribution_value is None: + min_contribution_value = FloatPrompt.ask( + f"Enter the minimum contribution amount in TAO " + f"[blue](>= {min_contribution.tao:,.4f})[/blue]" + ) + min_contribution = Balance.from_tao(min_contribution_value) + if min_contribution < min_contribution: + if prompt: + err_console.print( + f"[red]Minimum contribution must be at least " + f"{min_contribution.tao:,.4f} TAO.[/red]" + ) + min_contribution_value = None + continue + err_console.print( + "[red]Minimum contribution is below the chain's absolute minimum.[/red]" + ) + return False, "Minimum contribution is below the chain's absolute minimum." + break + + cap_value = cap_tao + while True: + if cap_value is None: + cap_value = FloatPrompt.ask( + f"Enter the cap amount in TAO [blue](> deposit of {deposit.tao:,.4f})[/blue]" + ) + cap = Balance.from_tao(cap_value) + if cap <= deposit: + if prompt: + err_console.print( + f"[red]Cap must be greater than the deposit ({deposit.tao:,.4f} TAO).[/red]" + ) + cap_value = None + continue + err_console.print( + "[red]Cap must be greater than the initial deposit.[/red]" + ) + return False, "Cap must be greater than the initial deposit." + break + + duration_value = duration_blocks + while True: + if duration_value is None: + duration_value = IntPrompt.ask( + f"Enter the crowdloan duration in blocks " + f"[blue]({min_duration} - {max_duration})[/blue]" + ) + if duration_value < min_duration or duration_value > max_duration: + if prompt: + err_console.print( + f"[red]Duration must be between {min_duration} and " + f"{max_duration} blocks.[/red]" + ) + duration_value = None + continue + err_console.print( + "[red]Crowdloan duration is outside the allowed range.[/red]" + ) + return False, "Crowdloan duration is outside the allowed range." + duration = duration_value + break + + if target_address and target_address.strip(): + target_address = target_address.strip() + if not is_valid_ss58_address(target_address): + err_console.print( + f"[red]Invalid target SS58 address provided: {target_address}[/red]" + ) + return False, "Invalid target SS58 address provided." + elif prompt: + target_input = Prompt.ask( + "Enter a target SS58 address (leave blank for none)", + default="", + show_default=False, + ) + target_address = target_input.strip() or None + + if target_address and not is_valid_ss58_address(target_address): + err_console.print( + f"[red]Invalid target SS58 address provided: {target_address}[/red]" + ) + return False, "Invalid target SS58 address provided." + + creator_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + if deposit > creator_balance: + err_console.print( + f"[red]Insufficient balance to cover the deposit. " + f"Available: {creator_balance}, required: {deposit}[/red]" + ) + return False, "Insufficient balance to cover the deposit." + + current_block = await subtensor.substrate.get_block_number(None) + end_block = current_block + duration + + call = await subtensor.substrate.compose_call( + call_module="Crowdloan", + call_function="create", + call_params={ + "deposit": deposit.rao, + "min_contribution": min_contribution.rao, + "cap": cap.rao, + "end": end_block, + "call": None, + "target_address": target_address, + }, + ) + + extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + + if prompt: + duration_text = blocks_to_duration(duration) + target_text = ( + target_address + if target_address + else f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]" + ) + + console.print( + f"You are about to create a crowdloan on " + f"[{COLORS.G.SUBHEAD_MAIN}]{subtensor.network}[/{COLORS.G.SUBHEAD_MAIN}]\n" + f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" + f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" + f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" + f" Duration: [bold]{duration}[/bold] blocks (~{duration_text})\n" + f" Ends at block: [bold]{end_block}[/bold]\n" + f" Target address: {target_text}\n" + f" Estimated fee: [{COLORS.P.TAO}]{extrinsic_fee}[/{COLORS.P.TAO}]" + ) + + if not Confirm.ask("Proceed with creating the crowdloan?"): + console.print("[yellow]Cancelled crowdloan creation.[/yellow]") + return False, "Cancelled crowdloan creation." + + success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + extrinsic_id = None + if extrinsic_receipt: + await print_extrinsic_id(extrinsic_receipt) + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + + if not success: + err_console.print( + f"[red]{error_message or 'Failed to create crowdloan.'}[/red]" + ) + return False, error_message or "Failed to create crowdloan." + + message = "Crowdloan created successfully." + console.print( + f"\n:white_check_mark: [green]{message}[/green]\n" + f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" + f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" + f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" + f" Ends at block: [bold]{end_block}[/bold]" + ) + if target_address: + console.print(f" Target address: {target_address}") + if extrinsic_id: + console.print(f" Extrinsic ID: [bold]{extrinsic_id}[/bold]") + + return True, message From 4529569dc7c956fced2db08bd4c89989ce836500 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 14:34:22 -0700 Subject: [PATCH 07/69] get_single_crowdloan --- .../src/bittensor/subtensor_interface.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 4fd80eb4..f5a7b7aa 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1720,6 +1720,35 @@ async def get_crowdloans( return crowdloans + async def get_single_crowdloan( + self, + crowdloan_id: int, + block_hash: Optional[str] = None, + ) -> Optional[CrowdloanData]: + """Retrieves detailed information about a specific crowdloan. + + Args: + crowdloan_id (int): The unique identifier of the crowdloan to retrieve. + block_hash (Optional[str]): The blockchain block hash at which to perform the query. + + Returns: + Optional[CrowdloanData]: A CrowdloanData object containing the crowdloan's details if found, + None if the crowdloan does not exist. + + The returned data includes crowdloan details such as funding targets, + contribution minimums, timeline, and current funding status + """ + crowdloan_info = await self.substrate.query( + module="Crowdloan", + storage_function="Crowdloans", + params=[crowdloan_id], + block_hash=block_hash, + ) + + if crowdloan_info: + return CrowdloanData.from_any(crowdloan_info) + return None + async def get_coldkey_swap_schedule_duration( self, block_hash: Optional[str] = None, From 734bff9b2dfe2721f4731e1a4456f41fbf1da63f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 14:34:31 -0700 Subject: [PATCH 08/69] get_crowdloan_contribution --- .../src/bittensor/subtensor_interface.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f5a7b7aa..058cb549 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1749,6 +1749,36 @@ async def get_single_crowdloan( return CrowdloanData.from_any(crowdloan_info) return None + async def get_crowdloan_contribution( + self, + crowdloan_id: int, + contributor: str, + block_hash: Optional[str] = None, + ) -> Optional[Balance]: + """Retrieves a user's contribution to a specific crowdloan. + + Args: + crowdloan_id (int): The ID of the crowdloan. + contributor (str): The SS58 address of the contributor. + block_hash (Optional[str]): The blockchain block hash at which to perform the query. + + Returns: + Optional[Balance]: The contribution amount as a Balance object if found, None otherwise. + + This function queries the Contributions storage to find the amount a specific address + has contributed to a given crowdloan. + """ + contribution = await self.substrate.query( + module="Crowdloan", + storage_function="Contributions", + params=[crowdloan_id, contributor], + block_hash=block_hash, + ) + + if contribution and contribution.value: + return Balance.from_rao(contribution.value) + return None + async def get_coldkey_swap_schedule_duration( self, block_hash: Optional[str] = None, From 51e6cd0efb2848b86299bae31e6b2094446fbad7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 15:22:03 -0700 Subject: [PATCH 09/69] list_crowdloans --- bittensor_cli/src/commands/crowd/view.py | 220 +++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 bittensor_cli/src/commands/crowd/view.py diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py new file mode 100644 index 00000000..87b2486e --- /dev/null +++ b/bittensor_cli/src/commands/crowd/view.py @@ -0,0 +1,220 @@ +import asyncio +from rich.table import Table + +from bittensor_cli.src import COLOR_PALETTE, COLORS +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.chain_data import CrowdloanData +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.utils import ( + blocks_to_duration, + console, + millify_tao, +) + + +def _shorten(account: str | None) -> str: + if not account: + return "-" + return f"{account[:6]}…{account[-6:]}" + + +def _status(loan: CrowdloanData, current_block: int) -> str: + if loan.finalized: + return "Finalized" + if loan.raised >= loan.cap: + return "Funded" + if current_block >= loan.end: + return "Closed" + return "Active" + + +def _time_remaining(loan: CrowdloanData, current_block: int) -> str: + diff = loan.end - current_block + if diff > 0: + return blocks_to_duration(diff) + if diff == 0: + return "due" + return f"Closed {blocks_to_duration(abs(diff))} ago" + + +async def list_crowdloans( + subtensor: SubtensorInterface, + verbose: bool = False, +) -> bool: + """List all crowdloans in a tabular format.""" + + current_block, loans = await asyncio.gather( + subtensor.substrate.get_block_number(None), + subtensor.get_crowdloans(), + ) + if not loans: + console.print("[yellow]No crowdloans found.[/yellow]") + return True + + total_raised = sum(loan.raised.tao for loan in loans.values()) + total_cap = sum(loan.cap.tao for loan in loans.values()) + total_loans = len(loans) + total_contributors = sum(loan.contributors_count for loan in loans.values()) + + funding_percentage = (total_raised / total_cap * 100) if total_cap > 0 else 0 + percentage_color = "dark_sea_green" if funding_percentage < 100 else "red" + formatted_percentage = ( + f"[{percentage_color}]{funding_percentage:.2f}%[/{percentage_color}]" + ) + + if not verbose: + funding_string = f"τ {millify_tao(total_raised)}/{millify_tao(total_cap)} ({formatted_percentage})" + else: + funding_string = ( + f"τ {total_raised:.1f}/{total_cap:.1f} ({formatted_percentage})" + ) + + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Crowdloans" + f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + + table.add_column( + "[bold white]ID", style="grey89", justify="center", footer=str(total_loans) + ) + table.add_column("[bold white]Status", style="cyan", justify="center") + table.add_column( + f"[bold white]Raised / Cap\n({Balance.get_unit(0)})", + style="dark_sea_green2", + justify="left", + footer=funding_string, + ) + table.add_column( + f"[bold white]Deposit\n({Balance.get_unit(0)})", + style="steel_blue3", + justify="left", + ) + table.add_column( + f"[bold white]Min Contribution\n({Balance.get_unit(0)})", + style=COLOR_PALETTE["POOLS"]["EMISSION"], + justify="left", + ) + table.add_column( + "[bold white]Ends (Block)", style=COLOR_PALETTE["STAKE"]["TAO"], justify="left" + ) + table.add_column( + "[bold white]Time Remaining", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + justify="left", + ) + table.add_column( + "[bold white]Contributors", + style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], + justify="center", + footer=str(total_contributors), + ) + table.add_column( + "[bold white]Creator", + style=COLOR_PALETTE["GENERAL"]["TEMPO"], + justify="left", + overflow="fold", + ) + table.add_column( + "[bold white]Target", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_EXTRA_1"], + justify="center", + ) + table.add_column( + "[bold white]Funds Account", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_EXTRA_2"], + justify="left", + overflow="fold", + ) + table.add_column("[bold white]Call", style="grey89", justify="center") + + sorted_loans = sorted( + loans.items(), + key=lambda x: ( + _status(x[1], current_block) != "Active", # Active loans first + -x[1].raised.tao, # Then by raised amount (descending) + ), + ) + + for loan_id, loan in sorted_loans: + status = _status(loan, current_block) + time_label = _time_remaining(loan, current_block) + + raised_cell = ( + f"τ {loan.raised.tao:,.4f} / τ {loan.cap.tao:,.4f}" + if verbose + else f"τ {millify_tao(loan.raised.tao)} / τ {millify_tao(loan.cap.tao)}" + ) + + deposit_cell = ( + f"τ {loan.deposit.tao:,.4f}" + if verbose + else f"τ {millify_tao(loan.deposit.tao)}" + ) + + min_contrib_cell = ( + f"τ {loan.min_contribution.tao:,.4f}" + if verbose + else f"τ {millify_tao(loan.min_contribution.tao)}" + ) + + status_color_map = { + "Finalized": COLOR_PALETTE["GENERAL"]["SUCCESS"], + "Funded": COLOR_PALETTE["POOLS"]["EMISSION"], + "Closed": COLOR_PALETTE["GENERAL"]["SYMBOL"], + "Active": COLOR_PALETTE["GENERAL"]["HINT"], + } + status_color = status_color_map.get(status, "white") + status_cell = f"[{status_color}]{status}[/{status_color}]" + + if "Closed" in time_label: + time_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{time_label}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + elif time_label == "due": + time_cell = f"[red]{time_label}[/red]" + else: + time_cell = time_label + + creator_cell = loan.creator if verbose else _shorten(loan.creator) + target_cell = ( + loan.target_address + if loan.target_address + else f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]" + ) + if not verbose and loan.target_address: + target_cell = _shorten(loan.target_address) + + funds_account_cell = ( + loan.funds_account if verbose else _shorten(loan.funds_account) + ) + + call_cell = ( + f"[{COLOR_PALETTE['GENERAL']['SUCCESS']}]Yes[/{COLOR_PALETTE['GENERAL']['SUCCESS']}]" + if loan.has_call + else f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]No[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + ) + + table.add_row( + str(loan_id), + status_cell, + raised_cell, + deposit_cell, + min_contrib_cell, + str(loan.end), + time_cell, + str(loan.contributors_count), + creator_cell, + target_cell, + funds_account_cell, + call_cell, + ) + + console.print(table) + + return True From b69c5ff47a4b8111b2fe4fece09a355892836dfd Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 15:38:19 -0700 Subject: [PATCH 10/69] show_crowdloan_details --- bittensor_cli/src/commands/crowd/view.py | 199 ++++++++++++++++++++++- 1 file changed, 198 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index 87b2486e..34603a00 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -1,4 +1,7 @@ +from typing import Optional + import asyncio +from bittensor_wallet import Wallet from rich.table import Table from bittensor_cli.src import COLOR_PALETTE, COLORS @@ -8,6 +11,7 @@ from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, + err_console, millify_tao, ) @@ -42,7 +46,7 @@ async def list_crowdloans( verbose: bool = False, ) -> bool: """List all crowdloans in a tabular format.""" - + current_block, loans = await asyncio.gather( subtensor.substrate.get_block_number(None), subtensor.get_crowdloans(), @@ -218,3 +222,196 @@ async def list_crowdloans( console.print(table) return True + + +async def show_crowdloan_details( + subtensor: SubtensorInterface, + crowdloan_id: int, + wallet: Optional[Wallet] = None, + verbose: bool = False, +) -> tuple[bool, str]: + """Display detailed information about a specific crowdloan.""" + + current_block, loan = await asyncio.gather( + subtensor.substrate.get_block_number(None), + subtensor.get_single_crowdloan(crowdloan_id), + ) + if not loan: + err_console.print(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") + return False, f"Crowdloan #{crowdloan_id} not found." + + user_contribution = None + if wallet and wallet.coldkeypub: + user_contribution = await subtensor.get_crowdloan_contribution( + crowdloan_id, wallet.coldkeypub.ss58_address + ) + + # Overview section + status = _status(loan, current_block) + status_color_map = { + "Finalized": COLOR_PALETTE["GENERAL"]["SUCCESS"], + "Funded": COLOR_PALETTE["POOLS"]["EMISSION"], + "Closed": COLOR_PALETTE["GENERAL"]["SYMBOL"], + "Active": COLOR_PALETTE["GENERAL"]["HINT"], + } + status_color = status_color_map.get(status, "white") + header = f"[bold white]CROWDLOAN #{crowdloan_id}[/bold white] - [{status_color}]{status.upper()}[/{status_color}]" + sections = [] + overview_lines = [ + f"[bold white]Status:[/bold white]\t\t[{status_color}]{status}[/{status_color}]", + ] + + if status == "Active": + overview_lines.append("\t\t\t[dim](accepting contributions)[/dim]") + elif status == "Funded": + overview_lines.append("\t\t\t[yellow](awaiting finalization)[/yellow]") + elif status == "Closed": + overview_lines.append("\t\t\t[dim](failed to reach cap)[/dim]") + elif status == "Finalized": + overview_lines.append("\t\t\t[green](successfully completed)[/green]") + + creator_display = loan.creator + funds_display = loan.funds_account + + overview_lines.extend( + [ + f"[bold white]Creator:[/bold white]\t\t[{COLOR_PALETTE['GENERAL']['TEMPO']}]{creator_display}[/{COLOR_PALETTE['GENERAL']['TEMPO']}]", + f"[bold white]Funds Account:[/bold white]\t[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]{funds_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]", + ] + ) + sections.append(("\n[bold cyan]OVERVIEW[/bold cyan]", "\n".join(overview_lines))) + + # Funding Progress section + raised_pct = (loan.raised.tao / loan.cap.tao * 100) if loan.cap.tao > 0 else 0 + progress_filled = int(raised_pct / 100 * 16) + progress_empty = 16 - progress_filled + progress_bar = f"[dark_sea_green]{'█' * progress_filled}[/dark_sea_green][grey35]{'░' * progress_empty}[/grey35]" + + if verbose: + raised_str = f"τ {loan.raised.tao:,.4f} / τ {loan.cap.tao:,.4f}" + deposit_str = f"τ {loan.deposit.tao:,.4f}" + min_contrib_str = f"τ {loan.min_contribution.tao:,.4f}" + else: + raised_str = f"τ {millify_tao(loan.raised.tao)} / τ {millify_tao(loan.cap.tao)}" + deposit_str = f"τ {millify_tao(loan.deposit.tao)}" + min_contrib_str = f"τ {millify_tao(loan.min_contribution.tao)}" + + funding_lines = [ + f"[bold white]Raised/Cap:[/bold white]\t{raised_str}", + f"[bold white]Progress:[/bold white]\t\t[{progress_bar}] [dark_sea_green]{raised_pct:.2f}%[/dark_sea_green]", + f"[bold white]Deposit:[/bold white]\t\t{deposit_str}", + f"[bold white]Min Contribution:[/bold white]\t{min_contrib_str}", + ] + + sections.append( + ("\n[bold cyan]FUNDING PROGRESS[/bold cyan]", "\n".join(funding_lines)) + ) + + # Timeline section + time_label = _time_remaining(loan, current_block) + if "Closed" in time_label: + time_display = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{time_label}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + elif time_label == "due": + time_display = "[red]Due now[/red]" + else: + time_display = f"[{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{time_label}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + + timeline_lines = [ + f"[bold white]Ends at Block:[/bold white]\t{loan.end}", + f"[bold white]Current Block:[/bold white]\t{current_block}", + f"[bold white]Time Remaining:[/bold white]\t{time_display}", + ] + sections.append(("\n[bold cyan]TIMELINE[/bold cyan]", "\n".join(timeline_lines))) + + # Participation section + participation_lines = [ + f"[bold white]Contributors:[/bold white]\t{loan.contributors_count}", + ] + + if loan.contributors_count > 0: + net_contributions = loan.raised.tao - loan.deposit.tao + avg_contribution = ( + net_contributions / (loan.contributors_count - 1) + if loan.contributors_count > 1 + else loan.deposit.tao + ) + if verbose: + avg_contrib_str = f"τ {avg_contribution:,.4f}" + else: + avg_contrib_str = f"τ {millify_tao(avg_contribution)}" + participation_lines.append( + f"[bold white]Avg Contribution:[/bold white]\t{avg_contrib_str}" + ) + + if user_contribution: + is_creator = wallet.coldkeypub.ss58_address == loan.creator + if verbose: + user_contrib_str = f"τ {user_contribution.tao:,.4f}" + else: + user_contrib_str = f"τ {millify_tao(user_contribution.tao)}" + + contrib_status = "" + if status == "Active" and not loan.finalized: + if is_creator and user_contribution.tao > loan.deposit.tao: + withdrawable = user_contribution.tao - loan.deposit.tao + if verbose: + withdrawable_str = f"τ {withdrawable:,.4f}" + else: + withdrawable_str = f"τ {millify_tao(withdrawable)}" + contrib_status = ( + f" [yellow](τ {withdrawable_str} withdrawable)[/yellow]" + ) + elif not is_creator: + contrib_status = " [yellow](withdrawable)[/yellow]" + elif status == "Closed": + contrib_status = " [green](refundable)[/green]" + + participation_lines.append( + f"[bold white]Your Contribution:[/bold white]\t{user_contrib_str}{contrib_status}" + ) + + if is_creator: + participation_lines.append("\t\t\t[dim](You are the creator)[/dim]") + + sections.append( + ("\n[bold cyan]PARTICIPATION[/bold cyan]", "\n".join(participation_lines)) + ) + + # Target section + target_lines = [] + + if loan.target_address: + target_display = loan.target_address + target_lines.append(f"[bold white]Address:[/bold white]\t\t{target_display}") + else: + target_lines.append( + f"[bold white]Address:[/bold white]\t\t[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]" + ) + + has_call_display = ( + f"[{COLOR_PALETTE['GENERAL']['SUCCESS']}]Yes[/{COLOR_PALETTE['GENERAL']['SUCCESS']}]" + if loan.has_call + else f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]No[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + ) + target_lines.append(f"[bold white]Has Call:[/bold white]\t\t{has_call_display}") + + sections.append(("\n[bold cyan]TARGET[/bold cyan]", "\n".join(target_lines))) + + # All sections + divider_width = 63 + divider = "═" * divider_width + header_text = f"CROWDLOAN #{crowdloan_id} - {status.upper()}" + padding_needed = (divider_width - len(header_text)) // 2 + centered_header = " " * padding_needed + header + + console.print(f"\n[bright_black]{divider}[/bright_black]") + console.print(centered_header) + console.print(f"[bright_black]{divider}[/bright_black]") + + for section_title, section_content in sections: + console.print(section_title) + console.print(section_content) + + console.print(f"[bright_black]{divider}[/bright_black]\n") + + return True, f"Displayed info for crowdloan #{crowdloan_id}" From 95f508f8ddfdca8faa0beda3cac8d5d5fd051e7c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 15:48:31 -0700 Subject: [PATCH 11/69] add to cli --- bittensor_cli/cli.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2c75e913..6454d4fd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -66,8 +66,11 @@ ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds -from bittensor_cli.src.commands.crowd import initiator as crowd_initiator from bittensor_cli.src.commands.liquidity import liquidity +from bittensor_cli.src.commands.crowd import ( + create as create_crowdloan, + view as view_crowdloan, +) from bittensor_cli.src.commands.liquidity.utils import ( prompt_liquidity, prompt_position_id, @@ -1170,6 +1173,9 @@ def __init__(self): self.liquidity_app.command( "remove", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] )(self.liquidity_remove) + self.crowd_app.command("list", rich_help_panel=HELP_PANELS["CROWD"]["INFO"])( + self.crowd_list + ) self.crowd_app.command( "create", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] )(self.crowd_create) @@ -7246,6 +7252,21 @@ def liquidity_modify( ) ) + def crowd_list( + self, + network: Optional[list[str]] = Options.network, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + self.verbosity_handler(quiet, verbose, False) + return self._run_command( + view_crowdloan.list_crowdloans( + subtensor=self.initialize_chain(network), + verbose=verbose, + ) + ) + def crowd_create( self, network: Optional[list[str]] = Options.network, @@ -7302,7 +7323,7 @@ def crowd_create( ) return self._run_command( - crowd_initiator.create_crowdloan( + create_crowdloan.create_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, deposit_tao=deposit, From 13cdbec6f3b062ecec548f67f8542ab63b153b10 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 15:48:40 -0700 Subject: [PATCH 12/69] wip --- bittensor_cli/cli.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6454d4fd..8e897c0c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1176,6 +1176,9 @@ def __init__(self): self.crowd_app.command("list", rich_help_panel=HELP_PANELS["CROWD"]["INFO"])( self.crowd_list ) + self.crowd_app.command("info", rich_help_panel=HELP_PANELS["CROWD"]["INFO"])( + self.crowd_info + ) self.crowd_app.command( "create", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] )(self.crowd_create) @@ -7267,6 +7270,52 @@ def crowd_list( ) ) + def crowd_info( + self, + crowdloan_id: Optional[int] = typer.Option( + None, + "--crowdloan-id", + "--crowdloan_id", + "--id", + help="The ID of the crowdloan to display", + ), + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """Display detailed information about a specific crowdloan.""" + self.verbosity_handler(quiet, verbose, 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, + ) + + wallet = None + if wallet_name or wallet_path or wallet_hotkey: + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[], + validate=WV.WALLET, + ) + + return self._run_command( + view_crowdloan.show_crowdloan_details( + subtensor=self.initialize_chain(network), + crowdloan_id=crowdloan_id, + wallet=wallet, + verbose=verbose, + ) + ) + def crowd_create( self, network: Optional[list[str]] = Options.network, From 1fdcf2d9dcf1a9c9759a43e47a0719bd93fb3133 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 16:47:34 -0700 Subject: [PATCH 13/69] update vars + params --- bittensor_cli/src/commands/crowd/view.py | 65 +++++++++++++----------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index 34603a00..e3161dbd 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -227,16 +227,19 @@ async def list_crowdloans( async def show_crowdloan_details( subtensor: SubtensorInterface, crowdloan_id: int, + crowdloan: Optional[CrowdloanData] = None, + current_block: Optional[int] = None, wallet: Optional[Wallet] = None, verbose: bool = False, ) -> tuple[bool, str]: """Display detailed information about a specific crowdloan.""" - current_block, loan = await asyncio.gather( - subtensor.substrate.get_block_number(None), - subtensor.get_single_crowdloan(crowdloan_id), - ) - if not loan: + if not crowdloan or not current_block: + current_block, crowdloan = await asyncio.gather( + subtensor.substrate.get_block_number(None), + subtensor.get_single_crowdloan(crowdloan_id), + ) + if not crowdloan: err_console.print(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") return False, f"Crowdloan #{crowdloan_id} not found." @@ -247,7 +250,7 @@ async def show_crowdloan_details( ) # Overview section - status = _status(loan, current_block) + status = _status(crowdloan, current_block) status_color_map = { "Finalized": COLOR_PALETTE["GENERAL"]["SUCCESS"], "Funded": COLOR_PALETTE["POOLS"]["EMISSION"], @@ -270,8 +273,8 @@ async def show_crowdloan_details( elif status == "Finalized": overview_lines.append("\t\t\t[green](successfully completed)[/green]") - creator_display = loan.creator - funds_display = loan.funds_account + creator_display = crowdloan.creator + funds_display = crowdloan.funds_account overview_lines.extend( [ @@ -282,19 +285,21 @@ async def show_crowdloan_details( sections.append(("\n[bold cyan]OVERVIEW[/bold cyan]", "\n".join(overview_lines))) # Funding Progress section - raised_pct = (loan.raised.tao / loan.cap.tao * 100) if loan.cap.tao > 0 else 0 + raised_pct = ( + (crowdloan.raised.tao / crowdloan.cap.tao * 100) if crowdloan.cap.tao > 0 else 0 + ) progress_filled = int(raised_pct / 100 * 16) progress_empty = 16 - progress_filled progress_bar = f"[dark_sea_green]{'█' * progress_filled}[/dark_sea_green][grey35]{'░' * progress_empty}[/grey35]" if verbose: - raised_str = f"τ {loan.raised.tao:,.4f} / τ {loan.cap.tao:,.4f}" - deposit_str = f"τ {loan.deposit.tao:,.4f}" - min_contrib_str = f"τ {loan.min_contribution.tao:,.4f}" + raised_str = f"τ {crowdloan.raised.tao:,.4f} / τ {crowdloan.cap.tao:,.4f}" + deposit_str = f"τ {crowdloan.deposit.tao:,.4f}" + min_contrib_str = f"τ {crowdloan.min_contribution.tao:,.4f}" else: - raised_str = f"τ {millify_tao(loan.raised.tao)} / τ {millify_tao(loan.cap.tao)}" - deposit_str = f"τ {millify_tao(loan.deposit.tao)}" - min_contrib_str = f"τ {millify_tao(loan.min_contribution.tao)}" + raised_str = f"τ {millify_tao(crowdloan.raised.tao)} / τ {millify_tao(crowdloan.cap.tao)}" + deposit_str = f"τ {millify_tao(crowdloan.deposit.tao)}" + min_contrib_str = f"τ {millify_tao(crowdloan.min_contribution.tao)}" funding_lines = [ f"[bold white]Raised/Cap:[/bold white]\t{raised_str}", @@ -308,7 +313,7 @@ async def show_crowdloan_details( ) # Timeline section - time_label = _time_remaining(loan, current_block) + time_label = _time_remaining(crowdloan, current_block) if "Closed" in time_label: time_display = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{time_label}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" elif time_label == "due": @@ -317,7 +322,7 @@ async def show_crowdloan_details( time_display = f"[{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{time_label}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" timeline_lines = [ - f"[bold white]Ends at Block:[/bold white]\t{loan.end}", + f"[bold white]Ends at Block:[/bold white]\t{crowdloan.end}", f"[bold white]Current Block:[/bold white]\t{current_block}", f"[bold white]Time Remaining:[/bold white]\t{time_display}", ] @@ -325,15 +330,15 @@ async def show_crowdloan_details( # Participation section participation_lines = [ - f"[bold white]Contributors:[/bold white]\t{loan.contributors_count}", + f"[bold white]Contributors:[/bold white]\t{crowdloan.contributors_count}", ] - if loan.contributors_count > 0: - net_contributions = loan.raised.tao - loan.deposit.tao + if crowdloan.contributors_count > 0: + net_contributions = crowdloan.raised.tao - crowdloan.deposit.tao avg_contribution = ( - net_contributions / (loan.contributors_count - 1) - if loan.contributors_count > 1 - else loan.deposit.tao + net_contributions / (crowdloan.contributors_count - 1) + if crowdloan.contributors_count > 1 + else crowdloan.deposit.tao ) if verbose: avg_contrib_str = f"τ {avg_contribution:,.4f}" @@ -344,16 +349,16 @@ async def show_crowdloan_details( ) if user_contribution: - is_creator = wallet.coldkeypub.ss58_address == loan.creator + is_creator = wallet.coldkeypub.ss58_address == crowdloan.creator if verbose: user_contrib_str = f"τ {user_contribution.tao:,.4f}" else: user_contrib_str = f"τ {millify_tao(user_contribution.tao)}" contrib_status = "" - if status == "Active" and not loan.finalized: - if is_creator and user_contribution.tao > loan.deposit.tao: - withdrawable = user_contribution.tao - loan.deposit.tao + if status == "Active" and not crowdloan.finalized: + if is_creator and user_contribution.tao > crowdloan.deposit.tao: + withdrawable = user_contribution.tao - crowdloan.deposit.tao if verbose: withdrawable_str = f"τ {withdrawable:,.4f}" else: @@ -380,8 +385,8 @@ async def show_crowdloan_details( # Target section target_lines = [] - if loan.target_address: - target_display = loan.target_address + if crowdloan.target_address: + target_display = crowdloan.target_address target_lines.append(f"[bold white]Address:[/bold white]\t\t{target_display}") else: target_lines.append( @@ -390,7 +395,7 @@ async def show_crowdloan_details( has_call_display = ( f"[{COLOR_PALETTE['GENERAL']['SUCCESS']}]Yes[/{COLOR_PALETTE['GENERAL']['SUCCESS']}]" - if loan.has_call + if crowdloan.has_call else f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]No[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" ) target_lines.append(f"[bold white]Has Call:[/bold white]\t\t{has_call_display}") From 477532165ac4f1a098de7e9d9aa0d4f13611d71a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 18:07:08 -0700 Subject: [PATCH 14/69] change to table --- bittensor_cli/src/commands/crowd/view.py | 154 +++++++++++------------ 1 file changed, 76 insertions(+), 78 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index e3161dbd..e1c2325b 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -2,7 +2,8 @@ import asyncio from bittensor_wallet import Wallet -from rich.table import Table +from rich import box +from rich.table import Column, Table from bittensor_cli.src import COLOR_PALETTE, COLORS from bittensor_cli.src.bittensor.balances import Balance @@ -249,7 +250,6 @@ async def show_crowdloan_details( crowdloan_id, wallet.coldkeypub.ss58_address ) - # Overview section status = _status(crowdloan, current_block) status_color_map = { "Finalized": COLOR_PALETTE["GENERAL"]["SUCCESS"], @@ -258,33 +258,55 @@ async def show_crowdloan_details( "Active": COLOR_PALETTE["GENERAL"]["HINT"], } status_color = status_color_map.get(status, "white") - header = f"[bold white]CROWDLOAN #{crowdloan_id}[/bold white] - [{status_color}]{status.upper()}[/{status_color}]" - sections = [] - overview_lines = [ - f"[bold white]Status:[/bold white]\t\t[{status_color}]{status}[/{status_color}]", - ] + table = Table( + Column( + "Field", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], + min_width=20, + no_wrap=True, + ), + Column("Value", style=COLOR_PALETTE["GENERAL"]["TEMPO"]), + title=f"\n[underline][{COLOR_PALETTE.G.HEADER}]CROWDLOAN #{crowdloan_id}[/underline][/{COLOR_PALETTE.G.HEADER}] - [{status_color} underline]{status.upper()}[/{status_color} underline]", + show_header=False, + show_footer=False, + width=None, + pad_edge=False, + box=box.SIMPLE, + show_edge=True, + border_style="bright_black", + expand=False, + ) + + # OVERVIEW Section + table.add_row("[cyan underline]OVERVIEW[/cyan underline]", "") + table.add_section() + + status_detail = "" if status == "Active": - overview_lines.append("\t\t\t[dim](accepting contributions)[/dim]") + status_detail = " [dim](accepting contributions)[/dim]" elif status == "Funded": - overview_lines.append("\t\t\t[yellow](awaiting finalization)[/yellow]") + status_detail = " [yellow](awaiting finalization)[/yellow]" elif status == "Closed": - overview_lines.append("\t\t\t[dim](failed to reach cap)[/dim]") + status_detail = " [dim](failed to reach cap)[/dim]" elif status == "Finalized": - overview_lines.append("\t\t\t[green](successfully completed)[/green]") + status_detail = " [green](successfully completed)[/green]" - creator_display = crowdloan.creator - funds_display = crowdloan.funds_account - - overview_lines.extend( - [ - f"[bold white]Creator:[/bold white]\t\t[{COLOR_PALETTE['GENERAL']['TEMPO']}]{creator_display}[/{COLOR_PALETTE['GENERAL']['TEMPO']}]", - f"[bold white]Funds Account:[/bold white]\t[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]{funds_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]", - ] + table.add_row("Status", f"[{status_color}]{status}[/{status_color}]{status_detail}") + table.add_row( + "Creator", + f"[{COLOR_PALETTE['GENERAL']['TEMPO']}]{crowdloan.creator}[/{COLOR_PALETTE['GENERAL']['TEMPO']}]", + ) + table.add_row( + "Funds Account", + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]{crowdloan.funds_account}[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]", ) - sections.append(("\n[bold cyan]OVERVIEW[/bold cyan]", "\n".join(overview_lines))) - # Funding Progress section + # FUNDING PROGRESS Section + table.add_section() + table.add_row("[cyan underline]FUNDING PROGRESS[/cyan underline]", "") + table.add_section() + raised_pct = ( (crowdloan.raised.tao / crowdloan.cap.tao * 100) if crowdloan.cap.tao > 0 else 0 ) @@ -301,18 +323,18 @@ async def show_crowdloan_details( deposit_str = f"τ {millify_tao(crowdloan.deposit.tao)}" min_contrib_str = f"τ {millify_tao(crowdloan.min_contribution.tao)}" - funding_lines = [ - f"[bold white]Raised/Cap:[/bold white]\t{raised_str}", - f"[bold white]Progress:[/bold white]\t\t[{progress_bar}] [dark_sea_green]{raised_pct:.2f}%[/dark_sea_green]", - f"[bold white]Deposit:[/bold white]\t\t{deposit_str}", - f"[bold white]Min Contribution:[/bold white]\t{min_contrib_str}", - ] - - sections.append( - ("\n[bold cyan]FUNDING PROGRESS[/bold cyan]", "\n".join(funding_lines)) + table.add_row("Raised/Cap", raised_str) + table.add_row( + "Progress", f"{progress_bar} [dark_sea_green]{raised_pct:.2f}%[/dark_sea_green]" ) + table.add_row("Deposit", deposit_str) + table.add_row("Min Contribution", min_contrib_str) + + # TIMELINE Section + table.add_section() + table.add_row("[cyan underline]TIMELINE[/cyan underline]", "") + table.add_section() - # Timeline section time_label = _time_remaining(crowdloan, current_block) if "Closed" in time_label: time_display = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{time_label}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" @@ -321,17 +343,16 @@ async def show_crowdloan_details( else: time_display = f"[{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{time_label}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - timeline_lines = [ - f"[bold white]Ends at Block:[/bold white]\t{crowdloan.end}", - f"[bold white]Current Block:[/bold white]\t{current_block}", - f"[bold white]Time Remaining:[/bold white]\t{time_display}", - ] - sections.append(("\n[bold cyan]TIMELINE[/bold cyan]", "\n".join(timeline_lines))) + table.add_row("Ends at Block", f"{crowdloan.end}") + table.add_row("Current Block", f"{current_block}") + table.add_row("Time Remaining", time_display) + + # PARTICIPATION Section + table.add_section() + table.add_row("[cyan underline]PARTICIPATION[/cyan underline]", "") + table.add_section() - # Participation section - participation_lines = [ - f"[bold white]Contributors:[/bold white]\t{crowdloan.contributors_count}", - ] + table.add_row("Contributors", f"{crowdloan.contributors_count}") if crowdloan.contributors_count > 0: net_contributions = crowdloan.raised.tao - crowdloan.deposit.tao @@ -344,9 +365,7 @@ async def show_crowdloan_details( avg_contrib_str = f"τ {avg_contribution:,.4f}" else: avg_contrib_str = f"τ {millify_tao(avg_contribution)}" - participation_lines.append( - f"[bold white]Avg Contribution:[/bold white]\t{avg_contrib_str}" - ) + table.add_row("Avg Contribution", avg_contrib_str) if user_contribution: is_creator = wallet.coldkeypub.ss58_address == crowdloan.creator @@ -371,52 +390,31 @@ async def show_crowdloan_details( elif status == "Closed": contrib_status = " [green](refundable)[/green]" - participation_lines.append( - f"[bold white]Your Contribution:[/bold white]\t{user_contrib_str}{contrib_status}" - ) - + your_contrib_value = f"{user_contrib_str}{contrib_status}" if is_creator: - participation_lines.append("\t\t\t[dim](You are the creator)[/dim]") + your_contrib_value += " [dim](You are the creator)[/dim]" + table.add_row("Your Contribution", your_contrib_value) - sections.append( - ("\n[bold cyan]PARTICIPATION[/bold cyan]", "\n".join(participation_lines)) - ) - - # Target section - target_lines = [] + # TARGET Section + table.add_section() + table.add_row("[cyan underline]TARGET[/cyan underline]", "") + table.add_section() if crowdloan.target_address: target_display = crowdloan.target_address - target_lines.append(f"[bold white]Address:[/bold white]\t\t{target_display}") else: - target_lines.append( - f"[bold white]Address:[/bold white]\t\t[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]" + target_display = ( + f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]" ) + table.add_row("Address", target_display) + has_call_display = ( f"[{COLOR_PALETTE['GENERAL']['SUCCESS']}]Yes[/{COLOR_PALETTE['GENERAL']['SUCCESS']}]" if crowdloan.has_call else f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]No[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" ) - target_lines.append(f"[bold white]Has Call:[/bold white]\t\t{has_call_display}") - - sections.append(("\n[bold cyan]TARGET[/bold cyan]", "\n".join(target_lines))) - - # All sections - divider_width = 63 - divider = "═" * divider_width - header_text = f"CROWDLOAN #{crowdloan_id} - {status.upper()}" - padding_needed = (divider_width - len(header_text)) // 2 - centered_header = " " * padding_needed + header - - console.print(f"\n[bright_black]{divider}[/bright_black]") - console.print(centered_header) - console.print(f"[bright_black]{divider}[/bright_black]") - - for section_title, section_content in sections: - console.print(section_title) - console.print(section_content) - - console.print(f"[bright_black]{divider}[/bright_black]\n") + table.add_row("Has Call", has_call_display) + console.print(table) return True, f"Displayed info for crowdloan #{crowdloan_id}" From 6ee1de3e099934b809ac2cec5122a5d5a7d3b1ab Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 18:16:52 -0700 Subject: [PATCH 15/69] updates creation summary --- bittensor_cli/src/commands/crowd/create.py | 40 +++++++++++++++------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 8835b2b7..be18b454 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -3,8 +3,9 @@ from bittensor_wallet import Wallet from rich.prompt import Confirm, IntPrompt, Prompt, FloatPrompt +from rich.table import Table, Column, box -from bittensor_cli.src import COLORS +from bittensor_cli.src import COLORS, COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( @@ -12,7 +13,6 @@ console, err_console, is_valid_ss58_address, - print_extrinsic_id, unlock_key, ) @@ -220,18 +220,33 @@ async def _get_constant(constant_name: str) -> int: else f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]" ) - console.print( - f"You are about to create a crowdloan on " - f"[{COLORS.G.SUBHEAD_MAIN}]{subtensor.network}[/{COLORS.G.SUBHEAD_MAIN}]\n" - f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" - f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" - f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" - f" Duration: [bold]{duration}[/bold] blocks (~{duration_text})\n" - f" Ends at block: [bold]{end_block}[/bold]\n" - f" Target address: {target_text}\n" - f" Estimated fee: [{COLORS.P.TAO}]{extrinsic_fee}[/{COLORS.P.TAO}]" + table = Table( + Column("[bold white]Field", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), + Column("[bold white]Value", style=COLOR_PALETTE["GENERAL"]["TEMPO"]), + title=f"\n[bold cyan]Crowdloan Creation Summary[/bold cyan]\n" + f"Network: [{COLORS.G.SUBHEAD_MAIN}]{subtensor.network}[/{COLORS.G.SUBHEAD_MAIN}]", + show_footer=False, + show_header=False, + width=None, + pad_edge=False, + box=box.SIMPLE, + show_edge=True, + border_style="bright_black", ) + table.add_row("Deposit", f"[{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]") + table.add_row( + "Min contribution", f"[{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]" + ) + table.add_row("Cap", f"[{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]") + table.add_row("Duration", f"[bold]{duration}[/bold] blocks (~{duration_text})") + table.add_row("Ends at block", f"[bold]{end_block}[/bold]") + table.add_row("Target address", target_text) + table.add_row( + "Estimated fee", f"[{COLORS.P.TAO}]{extrinsic_fee}[/{COLORS.P.TAO}]" + ) + console.print(table) + if not Confirm.ask("Proceed with creating the crowdloan?"): console.print("[yellow]Cancelled crowdloan creation.[/yellow]") return False, "Cancelled crowdloan creation." @@ -245,7 +260,6 @@ async def _get_constant(constant_name: str) -> int: extrinsic_id = None if extrinsic_receipt: - await print_extrinsic_id(extrinsic_receipt) extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() if not success: From 519f139e7b72d9fffc3b5550cc7684af0d935ea5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 18:22:40 -0700 Subject: [PATCH 16/69] add spacing --- bittensor_cli/src/bittensor/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index c8be3356..3846dc7b 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1504,11 +1504,11 @@ async def print_extrinsic_id( query = await substrate.rpc_request("system_chainType", []) if query.get("result") == "Live": console.print( - f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}: " + f":white_heavy_check_mark: Your extrinsic has been included as {ext_id}: " f"[blue]https://tao.app/extrinsic/{ext_id}[/blue]" ) return console.print( - f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}" + f":white_heavy_check_mark: Your extrinsic has been included as {ext_id}" ) return From 50d548b8350ed6817770222e2f443c50c8ca8aea Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 18:31:12 -0700 Subject: [PATCH 17/69] COLORS > COLOR_PALETTE --- bittensor_cli/src/commands/crowd/view.py | 62 ++++++++++++------------ 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index e1c2325b..9bf98b90 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -5,7 +5,7 @@ from rich import box from rich.table import Column, Table -from bittensor_cli.src import COLOR_PALETTE, COLORS +from bittensor_cli.src import COLORS from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import CrowdloanData from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -75,8 +75,8 @@ async def list_crowdloans( ) table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Crowdloans" - f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", + title=f"\n[{COLORS.G.HEADER}]Crowdloans" + f"\nNetwork: [{COLORS.G.SUBHEAD}]{subtensor.network}\n\n", show_footer=True, show_edge=False, header_style="bold white", @@ -104,37 +104,35 @@ async def list_crowdloans( ) table.add_column( f"[bold white]Min Contribution\n({Balance.get_unit(0)})", - style=COLOR_PALETTE["POOLS"]["EMISSION"], + style=COLORS.P.EMISSION, justify="left", ) - table.add_column( - "[bold white]Ends (Block)", style=COLOR_PALETTE["STAKE"]["TAO"], justify="left" - ) + table.add_column("[bold white]Ends (Block)", style=COLORS.S.TAO, justify="left") table.add_column( "[bold white]Time Remaining", - style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + style=COLORS.S.ALPHA, justify="left", ) table.add_column( "[bold white]Contributors", - style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], + style=COLORS.P.ALPHA_IN, justify="center", footer=str(total_contributors), ) table.add_column( "[bold white]Creator", - style=COLOR_PALETTE["GENERAL"]["TEMPO"], + style=COLORS.G.TEMPO, justify="left", overflow="fold", ) table.add_column( "[bold white]Target", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING_EXTRA_1"], + style=COLORS.G.SUBHEAD_EX_1, justify="center", ) table.add_column( "[bold white]Funds Account", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING_EXTRA_2"], + style=COLORS.G.SUBHEAD_EX_2, justify="left", overflow="fold", ) @@ -171,16 +169,16 @@ async def list_crowdloans( ) status_color_map = { - "Finalized": COLOR_PALETTE["GENERAL"]["SUCCESS"], - "Funded": COLOR_PALETTE["POOLS"]["EMISSION"], - "Closed": COLOR_PALETTE["GENERAL"]["SYMBOL"], - "Active": COLOR_PALETTE["GENERAL"]["HINT"], + "Finalized": COLORS.G.SUCCESS, + "Funded": COLORS.P.EMISSION, + "Closed": COLORS.G.SYM, + "Active": COLORS.G.HINT, } status_color = status_color_map.get(status, "white") status_cell = f"[{status_color}]{status}[/{status_color}]" if "Closed" in time_label: - time_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{time_label}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + time_cell = f"[{COLORS.G.SYM}]{time_label}[/{COLORS.G.SYM}]" elif time_label == "due": time_cell = f"[red]{time_label}[/red]" else: @@ -200,9 +198,9 @@ async def list_crowdloans( ) call_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SUCCESS']}]Yes[/{COLOR_PALETTE['GENERAL']['SUCCESS']}]" + f"[{COLORS.G.SUCCESS}]Yes[/{COLORS.G.SUCCESS}]" if loan.has_call - else f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]No[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + else f"[{COLORS.G.SYM}]No[/{COLORS.G.SYM}]" ) table.add_row( @@ -252,22 +250,22 @@ async def show_crowdloan_details( status = _status(crowdloan, current_block) status_color_map = { - "Finalized": COLOR_PALETTE["GENERAL"]["SUCCESS"], - "Funded": COLOR_PALETTE["POOLS"]["EMISSION"], - "Closed": COLOR_PALETTE["GENERAL"]["SYMBOL"], - "Active": COLOR_PALETTE["GENERAL"]["HINT"], + "Finalized": COLORS.G.SUCCESS, + "Funded": COLORS.P.EMISSION, + "Closed": COLORS.G.SYM, + "Active": COLORS.G.HINT, } status_color = status_color_map.get(status, "white") table = Table( Column( "Field", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], + style=COLORS.G.SUBHEAD, min_width=20, no_wrap=True, ), - Column("Value", style=COLOR_PALETTE["GENERAL"]["TEMPO"]), - title=f"\n[underline][{COLOR_PALETTE.G.HEADER}]CROWDLOAN #{crowdloan_id}[/underline][/{COLOR_PALETTE.G.HEADER}] - [{status_color} underline]{status.upper()}[/{status_color} underline]", + Column("Value", style=COLORS.G.TEMPO), + title=f"\n[underline][{COLORS.G.HEADER}]CROWDLOAN #{crowdloan_id}[/underline][/{COLORS.G.HEADER}] - [{status_color} underline]{status.upper()}[/{status_color} underline]", show_header=False, show_footer=False, width=None, @@ -295,11 +293,11 @@ async def show_crowdloan_details( table.add_row("Status", f"[{status_color}]{status}[/{status_color}]{status_detail}") table.add_row( "Creator", - f"[{COLOR_PALETTE['GENERAL']['TEMPO']}]{crowdloan.creator}[/{COLOR_PALETTE['GENERAL']['TEMPO']}]", + f"[{COLORS.G.TEMPO}]{crowdloan.creator}[/{COLORS.G.TEMPO}]", ) table.add_row( "Funds Account", - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]{crowdloan.funds_account}[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]", + f"[{COLORS.G.SUBHEAD_EX_2}]{crowdloan.funds_account}[/{COLORS.G.SUBHEAD_EX_2}]", ) # FUNDING PROGRESS Section @@ -337,11 +335,11 @@ async def show_crowdloan_details( time_label = _time_remaining(crowdloan, current_block) if "Closed" in time_label: - time_display = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{time_label}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + time_display = f"[{COLORS.G.SYM}]{time_label}[/{COLORS.G.SYM}]" elif time_label == "due": time_display = "[red]Due now[/red]" else: - time_display = f"[{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{time_label}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + time_display = f"[{COLORS.S.ALPHA}]{time_label}[/{COLORS.S.ALPHA}]" table.add_row("Ends at Block", f"{crowdloan.end}") table.add_row("Current Block", f"{current_block}") @@ -410,9 +408,9 @@ async def show_crowdloan_details( table.add_row("Address", target_display) has_call_display = ( - f"[{COLOR_PALETTE['GENERAL']['SUCCESS']}]Yes[/{COLOR_PALETTE['GENERAL']['SUCCESS']}]" + f"[{COLORS.G.SUCCESS}]Yes[/{COLORS.G.SUCCESS}]" if crowdloan.has_call - else f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]No[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + else f"[{COLORS.G.SYM}]No[/{COLORS.G.SYM}]" ) table.add_row("Has Call", has_call_display) From c5f22cdb5a77867da11f10ef268a5855e8cc7d32 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 18:37:16 -0700 Subject: [PATCH 18/69] update cli --- bittensor_cli/cli.py | 73 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8e897c0c..af6fb5d6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -68,6 +68,7 @@ from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.liquidity import liquidity from bittensor_cli.src.commands.crowd import ( + contribute as crowd_contribute, create as create_crowdloan, view as view_crowdloan, ) @@ -1149,6 +1150,9 @@ def __init__(self): self.app.add_typer( self.crowd_app, name="crowdloan", hidden=True, no_args_is_help=True ) + self.crowd_app.command( + "contribute", rich_help_panel=HELP_PANELS["CROWD"]["PARTICIPANT"] + )(self.crowd_contribute) # Liquidity self.app.add_typer( @@ -7262,6 +7266,7 @@ def crowd_list( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): + """List all crowdloans.""" self.verbosity_handler(quiet, verbose, False) return self._run_command( view_crowdloan.list_crowdloans( @@ -7360,7 +7365,7 @@ def crowd_create( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Create a new crowdloan.""" + """Start a new crowdloan campaign to raise funds for a subnet.""" self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( @@ -7387,6 +7392,72 @@ def crowd_create( ) ) + def crowd_contribute( + self, + crowdloan_id: Optional[int] = typer.Option( + None, + "--crowdloan-id", + "--crowdloan_id", + "--id", + help="The ID of the crowdloan to display", + ), + amount: Optional[float] = typer.Option( + None, + "--amount", + "-a", + help="Amount to contribute in TAO", + min=0.001, + ), + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """Contribute TAO to an active crowdloan. + + This command allows you to contribute TAO to a crowdloan that is currently accepting contributions. + The contribution will be automatically adjusted if it would exceed the crowdloan's cap. + + EXAMPLES + + [green]$[/green] btcli crowd contribute --id 0 --amount 100 + + [green]$[/green] btcli crowd contribute --id 1 + """ + self.verbosity_handler(quiet, verbose, 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, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + return self._run_command( + crowd_contribute.contribute_to_crowdloan( + subtensor=self.initialize_chain(network), + wallet=wallet, + crowdloan_id=crowdloan_id, + amount=amount, + prompt=prompt, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + ) + @staticmethod def convert( from_rao: Optional[str] = typer.Option( From 5de7c0e047b3aa9db5ce5f9eaec208692144410d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 18:37:28 -0700 Subject: [PATCH 19/69] colors --- bittensor_cli/src/commands/crowd/create.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index be18b454..1ea5f2f2 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -5,7 +5,7 @@ from rich.prompt import Confirm, IntPrompt, Prompt, FloatPrompt from rich.table import Table, Column, box -from bittensor_cli.src import COLORS, COLOR_PALETTE +from bittensor_cli.src import COLORS from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( @@ -221,8 +221,8 @@ async def _get_constant(constant_name: str) -> int: ) table = Table( - Column("[bold white]Field", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), - Column("[bold white]Value", style=COLOR_PALETTE["GENERAL"]["TEMPO"]), + Column("[bold white]Field", style=COLORS.G.SUBHEAD), + Column("[bold white]Value", style=COLORS.G.TEMPO), title=f"\n[bold cyan]Crowdloan Creation Summary[/bold cyan]\n" f"Network: [{COLORS.G.SUBHEAD_MAIN}]{subtensor.network}[/{COLORS.G.SUBHEAD_MAIN}]", show_footer=False, From a3fe558a4947c407dc0ccca7198149b1d58ffa59 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 18:37:50 -0700 Subject: [PATCH 20/69] wip --- .../src/commands/crowd/contribute.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 bittensor_cli/src/commands/crowd/contribute.py diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py new file mode 100644 index 00000000..67e8e7d3 --- /dev/null +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -0,0 +1,87 @@ +from typing import Optional + +from async_substrate_interface.utils.cache import asyncio +from bittensor_wallet import Wallet +from rich import box +from rich.prompt import Confirm, FloatPrompt +from rich.table import Column, Table + +from bittensor_cli.src import COLORS +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + print_extrinsic_id, + unlock_key, +) +from bittensor_cli.src.commands.crowd.view import show_crowdloan_details +from bittensor_cli.src.bittensor.chain_data import CrowdloanData + + +def validate_for_contribution( + crowdloan: CrowdloanData, + crowdloan_id: int, + current_block: int, +) -> tuple[bool, Optional[str]]: + """Validate if a crowdloan can accept contributions. + + Args: + crowdloan: The crowdloan data object + crowdloan_id: The ID of the crowdloan + current_block: Current blockchain block number + + Returns: + tuple[bool, Optional[str]]: (is_valid, error_message) + - If valid: (True, None) + - If invalid: (False, error_message) + """ + if crowdloan.finalized: + return False, f"Crowdloan #{crowdloan_id} is already finalized." + + if current_block >= crowdloan.end: + return False, f"Crowdloan #{crowdloan_id} has ended." + + if crowdloan.raised >= crowdloan.cap: + return False, f"Crowdloan #{crowdloan_id} has reached its cap." + + return True, None + + +async def contribute_to_crowdloan( + subtensor: SubtensorInterface, + wallet: Wallet, + crowdloan_id: int, + amount: Optional[float], + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, +) -> tuple[bool, str]: + """Contribute TAO to an active crowdloan. + + Args: + subtensor: SubtensorInterface object for chain interaction + wallet: Wallet object containing coldkey for contribution + crowdloan_id: ID of the crowdloan to contribute to + amount: Amount to contribute in TAO (None to prompt) + prompt: Whether to prompt for confirmation + wait_for_inclusion: Wait for transaction inclusion + wait_for_finalization: Wait for transaction finalization + + Returns: + tuple[bool, str]: Success status and message + """ + crowdloan, current_block = await asyncio.gather( + subtensor.get_single_crowdloan(crowdloan_id), + subtensor.substrate.get_block_number(None), + ) + if not crowdloan: + err_console.print(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") + return False, f"Crowdloan #{crowdloan_id} not found." + + is_valid, error_message = validate_for_contribution( + crowdloan, crowdloan_id, current_block + ) + if not is_valid: + err_console.print(f"[red]{error_message}[/red]") + return False, error_message From 7ec90911ce62e5ab05ba2e44b482ed29973a18c4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 14 Oct 2025 18:38:06 -0700 Subject: [PATCH 21/69] validate_for_contribution --- .../src/commands/crowd/contribute.py | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index 67e8e7d3..2dbdf9d6 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -85,3 +85,183 @@ async def contribute_to_crowdloan( if not is_valid: err_console.print(f"[red]{error_message}[/red]") return False, error_message + + contributor_address = wallet.coldkeypub.ss58_address + current_contribution, user_balance, _ = await asyncio.gather( + subtensor.get_crowdloan_contribution(crowdloan_id, contributor_address), + subtensor.get_balance(contributor_address), + show_crowdloan_details( + subtensor=subtensor, + crowdloan_id=crowdloan_id, + wallet=wallet, + verbose=False, + crowdloan=crowdloan, + current_block=current_block, + ), + ) + + if amount is None: + left_to_raise = crowdloan.cap - crowdloan.raised + max_contribution = min(user_balance, left_to_raise) + + console.print( + f"\n[bold cyan]Contribution Options:[/bold cyan]\n" + f" Your Balance: {user_balance}\n" + f" Maximum You Can Contribute: [{COLORS.S.AMOUNT}]{max_contribution}[/{COLORS.S.AMOUNT}]" + ) + amount = FloatPrompt.ask( + f"\nEnter contribution amount in {Balance.unit}", + default=float(crowdloan.min_contribution.tao), + ) + + contribution_amount = Balance.from_tao(amount) + if contribution_amount < crowdloan.min_contribution: + err_console.print( + f"[red]Contribution amount ({contribution_amount}) is below minimum ({crowdloan.min_contribution}).[/red]" + ) + return False, "Contribution below minimum requirement." + + if contribution_amount > user_balance: + err_console.print( + f"[red]Insufficient balance. You have {user_balance} but trying to contribute {contribution_amount}.[/red]" + ) + return False, "Insufficient balance." + + # Auto-adjustment + left_to_raise = crowdloan.cap - crowdloan.raised + actual_contribution = contribution_amount + will_be_adjusted = False + + if contribution_amount > left_to_raise: + actual_contribution = left_to_raise + will_be_adjusted = True + + # Extrinsic fee + call = await subtensor.substrate.compose_call( + call_module="Crowdloan", + call_function="contribute", + call_params={ + "crowdloan_id": crowdloan_id, + "amount": contribution_amount.rao, + }, + ) + extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + + table = Table( + Column("[bold white]Field", style=COLORS.G.SUBHEAD), + Column("[bold white]Value", style=COLORS.G.TEMPO), + title="\n[bold cyan]Contribution Summary[/bold cyan]", + show_footer=False, + width=None, + pad_edge=False, + box=box.SIMPLE, + show_edge=True, + border_style="bright_black", + ) + + table.add_row("Crowdloan ID", str(crowdloan_id)) + table.add_row("Creator", crowdloan.creator) + table.add_row( + "Current Progress", + f"{crowdloan.raised} / {crowdloan.cap} ({(crowdloan.raised.tao / crowdloan.cap.tao * 100):.2f}%)", + ) + + if current_contribution: + table.add_row("Your Current Contribution", str(current_contribution)) + table.add_row("New Contribution", str(actual_contribution)) + table.add_row( + "Total After Contribution", + f"[{COLORS.S.AMOUNT}]{Balance.from_rao(current_contribution.rao + actual_contribution.rao)}[/{COLORS.S.AMOUNT}]", + ) + else: + table.add_row( + "Contribution Amount", + f"[{COLORS.S.AMOUNT}]{actual_contribution}[/{COLORS.S.AMOUNT}]", + ) + + if will_be_adjusted: + table.add_row( + "Note", + f"[yellow]Amount adjusted from {contribution_amount} to {actual_contribution} (cap limit)[/yellow]", + ) + + table.add_row("Transaction Fee", str(extrinsic_fee)) + table.add_row( + "Balance After", + f"[blue]{user_balance}[/blue] → [{COLORS.S.AMOUNT}]{Balance.from_rao(user_balance.rao - actual_contribution.rao - extrinsic_fee.rao)}[/{COLORS.S.AMOUNT}]", + ) + console.print(table) + + if will_be_adjusted: + console.print( + f"\n[yellow] Your contribution will be automatically adjusted to {actual_contribution} " + f"because the crowdloan only needs {left_to_raise} more to reach its cap.[/yellow]" + ) + + if prompt: + if not Confirm.ask("\nProceed with contribution?"): + console.print("[yellow]Contribution cancelled.[/yellow]") + return False, "Contribution cancelled by user." + + unlock_status = unlock_key(wallet) + if not unlock_status.success: + err_console.print(f"[red]{unlock_status.message}[/red]") + return False, unlock_status.message + + with console.status(f"\n:satellite: Contributing to crowdloan #{crowdloan_id}..."): + ( + success, + error_message, + extrinsic_receipt, + ) = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not success: + err_console.print(f"[red]Failed to contribute: {error_message}[/red]") + return False, error_message or "Failed to contribute." + + new_balance, new_contribution, updated_crowdloan = await asyncio.gather( + subtensor.get_balance(contributor_address), + subtensor.get_crowdloan_contribution(crowdloan_id, contributor_address), + subtensor.get_single_crowdloan(crowdloan_id), + ) + + console.print( + f"\n:white_heavy_check_mark: [dark_sea_green3]Successfully contributed to crowdloan #{crowdloan_id}![/dark_sea_green3]" + ) + + console.print( + f"Balance:\n [blue]{user_balance}[/blue] → " + f"[{COLORS.S.AMOUNT}]{new_balance}[/{COLORS.S.AMOUNT}]" + ) + + if new_contribution: + if current_contribution: + console.print( + f"Your Contribution:\n [blue]{current_contribution}[/blue] → " + f"[{COLORS.S.AMOUNT}]{new_contribution}[/{COLORS.S.AMOUNT}]" + ) + else: + console.print( + f"Your Contribution: [{COLORS.S.AMOUNT}]{new_contribution}[/{COLORS.S.AMOUNT}]" + ) + + if updated_crowdloan: + console.print( + f"Crowdloan Progress:\n [blue]{crowdloan.raised}[/blue] → " + f"[{COLORS.S.AMOUNT}]{updated_crowdloan.raised}[/{COLORS.S.AMOUNT}] / {updated_crowdloan.cap}" + ) + + if updated_crowdloan.raised >= updated_crowdloan.cap: + console.print( + "\n[bold green]🎉 Crowdloan has reached its funding cap![/bold green]" + ) + + if extrinsic_receipt: + await print_extrinsic_id(extrinsic_receipt) + + return True, "Successfully contributed to crowdloan." From dbcfa902f8255652f4d5b6d64c2ae32baf5fc9b2 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 09:59:14 -0700 Subject: [PATCH 22/69] fix fmt --- bittensor_cli/src/commands/crowd/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index 9bf98b90..96950706 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -377,9 +377,9 @@ async def show_crowdloan_details( if is_creator and user_contribution.tao > crowdloan.deposit.tao: withdrawable = user_contribution.tao - crowdloan.deposit.tao if verbose: - withdrawable_str = f"τ {withdrawable:,.4f}" + withdrawable_str = f"{withdrawable:,.4f}" else: - withdrawable_str = f"τ {millify_tao(withdrawable)}" + withdrawable_str = f"{millify_tao(withdrawable)}" contrib_status = ( f" [yellow](τ {withdrawable_str} withdrawable)[/yellow]" ) From 2cd942783d7cff63a2696f9716533816a5571f31 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 10:00:27 -0700 Subject: [PATCH 23/69] withdraw_from_crowdloan --- .../src/commands/crowd/contribute.py | 194 +++++++++++++++++- 1 file changed, 192 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index 2dbdf9d6..b0809f0b 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -146,6 +146,7 @@ async def contribute_to_crowdloan( }, ) extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + updated_balance = user_balance - actual_contribution - extrinsic_fee table = Table( Column("[bold white]Field", style=COLORS.G.SUBHEAD), @@ -188,7 +189,7 @@ async def contribute_to_crowdloan( table.add_row("Transaction Fee", str(extrinsic_fee)) table.add_row( "Balance After", - f"[blue]{user_balance}[/blue] → [{COLORS.S.AMOUNT}]{Balance.from_rao(user_balance.rao - actual_contribution.rao - extrinsic_fee.rao)}[/{COLORS.S.AMOUNT}]", + f"[blue]{user_balance}[/blue] → [{COLORS.S.AMOUNT}]{updated_balance}[/{COLORS.S.AMOUNT}]", ) console.print(table) @@ -231,7 +232,7 @@ async def contribute_to_crowdloan( ) console.print( - f"\n:white_heavy_check_mark: [dark_sea_green3]Successfully contributed to crowdloan #{crowdloan_id}![/dark_sea_green3]" + f"\n[dark_sea_green3]Successfully contributed to crowdloan #{crowdloan_id}![/dark_sea_green3]" ) console.print( @@ -265,3 +266,192 @@ async def contribute_to_crowdloan( await print_extrinsic_id(extrinsic_receipt) return True, "Successfully contributed to crowdloan." + + +async def withdraw_from_crowdloan( + subtensor: SubtensorInterface, + wallet: Wallet, + crowdloan_id: int, + wait_for_inclusion: bool, + wait_for_finalization: bool, + prompt: bool, +) -> tuple[bool, str]: + """ + Withdraw contributions from a non-finalized crowdloan. + + Non-creators can withdraw their full contribution. + Creators can only withdraw amounts above their initial deposit. + + Args: + subtensor: SubtensorInterface instance for blockchain interaction + wallet: Wallet instance containing the user's keys + crowdloan_id: The ID of the crowdloan to withdraw from + wait_for_inclusion: Whether to wait for transaction inclusion + wait_for_finalization: Whether to wait for transaction finalization + prompt: Whether to prompt for user confirmation + + Returns: + Tuple of (success, message) indicating the result + """ + + crowdloan, current_block = await asyncio.gather( + subtensor.get_single_crowdloan(crowdloan_id), + subtensor.substrate.get_block_number(None), + ) + + if not crowdloan: + err_console.print(f"[red]Crowdloan #{crowdloan_id} does not exist.[/red]") + return False, f"Crowdloan #{crowdloan_id} does not exist." + + if crowdloan.finalized: + err_console.print( + f"[red]Crowdloan #{crowdloan_id} is already finalized. Withdrawals are not allowed.[/red]" + ) + return False, "Cannot withdraw from finalized crowdloan." + + user_contribution, user_balance = await asyncio.gather( + subtensor.get_crowdloan_contribution( + crowdloan_id, wallet.coldkeypub.ss58_address + ), + subtensor.get_balance(wallet.coldkeypub.ss58_address), + ) + + if user_contribution == Balance.from_tao(0): + err_console.print( + f"[red]You have no contribution to withdraw from crowdloan #{crowdloan_id}.[/red]" + ) + return False, "No contribution to withdraw." + + is_creator = wallet.coldkeypub.ss58_address == crowdloan.creator + if is_creator: + withdrawable = user_contribution - crowdloan.deposit + if withdrawable <= 0: + err_console.print( + f"[red]As the creator, you cannot withdraw your deposit of {crowdloan.deposit}. " + f"Only contributions above the deposit can be withdrawn.[/red]" + ) + return False, "Creator cannot withdraw deposit amount." + remaining_contribution = crowdloan.deposit + else: + withdrawable = user_contribution + remaining_contribution = Balance.from_tao(0) + + call = await subtensor.substrate.compose_call( + call_module="Crowdloan", + call_function="withdraw", + call_params={ + "crowdloan_id": crowdloan_id, + }, + ) + extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + await show_crowdloan_details( + subtensor=subtensor, + crowdloan_id=crowdloan_id, + wallet=wallet, + verbose=False, + crowdloan=crowdloan, + current_block=current_block, + ) + + if prompt: + new_balance = user_balance + withdrawable - extrinsic_fee + new_raised = crowdloan.raised - withdrawable + table = Table( + Column("[bold white]Field", style=COLORS.G.SUBHEAD), + Column("[bold white]Value", style=COLORS.G.TEMPO), + title="\n[bold cyan]Withdrawal Summary[/bold cyan]", + show_footer=False, + show_header=False, + width=None, + pad_edge=False, + box=box.SIMPLE, + show_edge=True, + border_style="bright_black", + ) + + table.add_row("Crowdloan ID", str(crowdloan_id)) + + if is_creator: + table.add_row("Role", "[yellow]Creator[/yellow]") + table.add_row("Current Contribution", str(user_contribution)) + table.add_row("Deposit (Locked)", f"[yellow]{crowdloan.deposit}[/yellow]") + table.add_row( + "Withdrawable Amount", + f"[{COLORS.S.AMOUNT}]{withdrawable}[/{COLORS.S.AMOUNT}]", + ) + table.add_row( + "Remaining After Withdrawal", + f"[yellow]{remaining_contribution}[/yellow] (deposit)", + ) + else: + table.add_row("Current Contribution", str(user_contribution)) + table.add_row( + "Withdrawal Amount", + f"[{COLORS.S.AMOUNT}]{withdrawable}[/{COLORS.S.AMOUNT}]", + ) + + table.add_row("Transaction Fee", str(extrinsic_fee)) + table.add_row( + "Balance After", + f"[blue]{user_balance}[/blue] → [{COLORS.S.AMOUNT}]{new_balance}[/{COLORS.S.AMOUNT}]", + ) + + table.add_row( + "Crowdloan Total After", + f"[blue]{crowdloan.raised}[/blue] → [{COLORS.S.AMOUNT}]{new_raised}[/{COLORS.S.AMOUNT}]", + ) + + console.print(table) + + if not Confirm.ask("\nProceed with withdrawal?"): + console.print("[yellow]Withdrawal cancelled.[/yellow]") + return False, "Withdrawal cancelled by user." + + unlock_status = unlock_key(wallet) + if not unlock_status.success: + err_console.print(f"[red]{unlock_status.message}[/red]") + return False, unlock_status.message + + with console.status(f"\n:satellite: Withdrawing from crowdloan #{crowdloan_id}..."): + ( + success, + error_message, + extrinsic_receipt, + ) = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not success: + err_console.print( + f"[red]Failed to withdraw: {error_message or 'Unknown error'}[/red]" + ) + return False, error_message or "Failed to withdraw from crowdloan." + + console.print( + f"\n✅ [green]Successfully withdrew from crowdloan #{crowdloan_id}![/green]\n" + ) + + new_balance, updated_contribution = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_crowdloan_contribution( + crowdloan_id, wallet.coldkeypub.ss58_address + ), + ) + + console.print( + f"Amount Withdrawn: [{COLORS.S.AMOUNT}]{withdrawable}[/{COLORS.S.AMOUNT}]\n" + f"Balance:\n [blue]{user_balance}[/blue] → [{COLORS.S.AMOUNT}]{new_balance}[/{COLORS.S.AMOUNT}]" + ) + + if is_creator and updated_contribution: + console.print( + f"Remaining Contribution: [{COLORS.S.AMOUNT}]{updated_contribution}[/{COLORS.S.AMOUNT}] (deposit locked)" + ) + + if extrinsic_receipt: + await print_extrinsic_id(extrinsic_receipt) + + return True, "Successfully withdrew from crowdloan." From b1dadbd60ea690dcbd73a39167ff5e7458beba6e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 10:01:20 -0700 Subject: [PATCH 24/69] add withdraw cmd --- bittensor_cli/cli.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index af6fb5d6..c4b60f48 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1153,6 +1153,9 @@ def __init__(self): self.crowd_app.command( "contribute", rich_help_panel=HELP_PANELS["CROWD"]["PARTICIPANT"] )(self.crowd_contribute) + self.crowd_app.command( + "withdraw", rich_help_panel=HELP_PANELS["CROWD"]["PARTICIPANT"] + )(self.crowd_withdraw) # Liquidity self.app.add_typer( @@ -7458,6 +7461,57 @@ def crowd_contribute( ) ) + def crowd_withdraw( + self, + crowdloan_id: Optional[int] = typer.Option( + None, + "--crowdloan-id", + "--crowdloan_id", + "--id", + help="The ID of the crowdloan to withdraw from", + ), + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Withdraw contributions from a non-finalized crowdloan. + + Non-creators can withdraw their full contribution. + Creators can only withdraw amounts above their initial deposit. + """ + 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, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + return self._run_command( + crowd_contribute.withdraw_from_crowdloan( + subtensor=self.initialize_chain(network), + wallet=wallet, + crowdloan_id=crowdloan_id, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + ) + @staticmethod def convert( from_rao: Optional[str] = typer.Option( From 79f5aaa5d3a5af6fc6b794c5bbd1fd301188c8ff Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 15:49:56 -0700 Subject: [PATCH 25/69] add util --- bittensor_cli/src/bittensor/utils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 3846dc7b..44647530 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1426,6 +1426,28 @@ def hex_to_bytes(hex_str: str) -> bytes: return bytes_result +def flatten_inline_call(raw: Any) -> bytes: + """ + Convert SCALE inline call payloads to bytes. + """ + + def _to_bytes(value: Any) -> bytes: + if isinstance(value, (bytes, bytearray)): + return bytes(value) + if isinstance(value, int): + if not 0 <= value <= 255: + raise ValueError(f"Byte out of range: {value}") + return bytes((value,)) + if isinstance(value, str): + hex_str = value[2:] if value.startswith("0x") else value + return bytes.fromhex(hex_str) + if isinstance(value, (tuple, list)): + return b"".join(_to_bytes(item) for item in value) + raise TypeError(f"Unsupported call encoding: {type(value)!r}") + + return _to_bytes(raw) + + def blocks_to_duration(blocks: int) -> str: """Convert blocks to human readable duration string using two largest units. From 550c25cb65a19aebfd2e42441348a495898058fc Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 15:50:20 -0700 Subject: [PATCH 26/69] update ds --- bittensor_cli/src/bittensor/chain_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index e3ff542e..5c3f3639 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1228,6 +1228,7 @@ class CrowdloanData(InfoBase): contributors_count: int target_address: Optional[str] has_call: bool + call_details: Optional[dict] = None @classmethod def _fix_decoded(cls, decoded: dict[str, Any]) -> "CrowdloanData": @@ -1254,4 +1255,5 @@ def _fix_decoded(cls, decoded: dict[str, Any]) -> "CrowdloanData": contributors_count=int(decoded["contributors_count"]), target_address=target_address, has_call=bool(decoded["call"]), + call_details=decoded["call_details"], ) From d905254993bd90dc6418c9bb0f8a882c514988e0 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 15:50:45 -0700 Subject: [PATCH 27/69] update crowdloan getters --- .../src/bittensor/subtensor_interface.py | 77 ++++++++++++++++++- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 058cb549..724e48b8 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -14,7 +14,7 @@ from bittensor_wallet import Wallet from bittensor_wallet.bittensor_wallet import Keypair from bittensor_wallet.utils import SS58_FORMAT -from scalecodec import GenericCall +from scalecodec import GenericCall, ScaleBytes import typer import websockets @@ -44,6 +44,7 @@ u16_normalized_float, U16_MAX, get_hotkey_pub_ss58, + flatten_inline_call, ) @@ -168,6 +169,66 @@ async def query( else: return result + async def _decode_inline_call( + self, + call_option: Any, + block_hash: Optional[str] = None, + ) -> Optional[dict[str, Any]]: + """ + Decode an `Option` returned from storage into a structured dictionary. + """ + if not call_option: + return None + + if isinstance(call_option, dict) and { + "call_module", + "call_function", + }.issubset(call_option.keys()): + return { + "call_index": call_option.get("call_index"), + "pallet": call_option.get("call_module"), + "method": call_option.get("call_function"), + "args": call_option.get("call_args", []), + "hash": call_option.get("call_hash"), + } + + inline_payload = None + if isinstance(call_option, dict): + inline_payload = call_option.get("Inline") or call_option.get("inline") + else: + inline_payload = call_option + + if inline_payload is None: + return None + + call_bytes = flatten_inline_call(inline_payload) + call_obj = await self.substrate.create_scale_object( + "Call", + data=ScaleBytes(call_bytes), + block_hash=block_hash, + ) + call_value = call_obj.decode() + + if not isinstance(call_value, dict): + return None + + call_args = call_value.get("call_args") or [] + args_map: dict[str, dict[str, Any]] = {} + for arg in call_args: + if isinstance(arg, dict) and arg.get("name"): + args_map[arg["name"]] = { + "type": arg.get("type"), + "value": arg.get("value"), + } + + return { + "call_index": call_value.get("call_index"), + "pallet": call_value.get("call_module"), + "method": call_value.get("call_function"), + "args": args_map, + "hash": call_value.get("call_hash"), + } + async def get_all_subnet_netuids( self, block_hash: Optional[str] = None ) -> list[int]: @@ -1716,7 +1777,13 @@ async def get_crowdloans( ) crowdloans = {} async for fund_id, fund_info in crowdloans_data: - crowdloans[fund_id] = CrowdloanData.from_any(fund_info) + decoded_call = await self._decode_inline_call( + fund_info["call"], + block_hash=block_hash, + ) + info_dict = dict(fund_info.value) + info_dict["call_details"] = decoded_call + crowdloans[fund_id] = CrowdloanData.from_any(info_dict) return crowdloans @@ -1744,8 +1811,12 @@ async def get_single_crowdloan( params=[crowdloan_id], block_hash=block_hash, ) - if crowdloan_info: + decoded_call = await self._decode_inline_call( + crowdloan_info.get("call"), + block_hash=block_hash, + ) + crowdloan_info["call_details"] = decoded_call return CrowdloanData.from_any(crowdloan_info) return None From 45203723ad65fb2252fa44434dfc0f0bd1cb2fc3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 15:51:13 -0700 Subject: [PATCH 28/69] update view functions --- bittensor_cli/src/commands/crowd/view.py | 63 ++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index 96950706..50c0967a 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -197,11 +197,24 @@ async def list_crowdloans( loan.funds_account if verbose else _shorten(loan.funds_account) ) - call_cell = ( - f"[{COLORS.G.SUCCESS}]Yes[/{COLORS.G.SUCCESS}]" - if loan.has_call - else f"[{COLORS.G.SYM}]No[/{COLORS.G.SYM}]" - ) + if loan.call_details: + pallet = loan.call_details.get("pallet", "") + method = loan.call_details.get("method", "") + + if pallet == "SubtensorModule" and method == "register_leased_network": + call_label = "[magenta]Subnet Leasing[/magenta]" + else: + call_label = ( + f"{pallet}.{method}" + if pallet and method + else method or pallet or "Unknown" + ) + + call_cell = call_label + elif loan.has_call: + call_cell = f"[{COLORS.G.SYM}]Unknown[/{COLORS.G.SYM}]" + else: + call_cell = "-" table.add_row( str(loan_id), @@ -414,5 +427,45 @@ async def show_crowdloan_details( ) table.add_row("Has Call", has_call_display) + if crowdloan.has_call and crowdloan.call_details: + table.add_section() + table.add_row("[cyan underline]CALL DETAILS[/cyan underline]", "") + table.add_section() + + pallet = crowdloan.call_details.get("pallet", "Unknown") + method = crowdloan.call_details.get("method", "Unknown") + args = crowdloan.call_details.get("args", {}) + + if pallet == "SubtensorModule" and method == "register_leased_network": + table.add_row("Type", "[magenta]Subnet Leasing[/magenta]") + emissions_share = args.get("emissions_share", {}).get("value") + if emissions_share is not None: + table.add_row("Emissions Share", f"[cyan]{emissions_share}%[/cyan]") + + end_block = args.get("end_block", {}).get("value") + if end_block: + table.add_row("Lease Ends", f"Block {end_block}") + else: + table.add_row("Lease Duration", "[green]Perpetual[/green]") + else: + table.add_row("Pallet", pallet) + table.add_row("Method", method) + if args: + for arg_name, arg_data in args.items(): + if isinstance(arg_data, dict): + display_value = arg_data.get("value") + arg_type = arg_data.get("type") + else: + display_value = arg_data + arg_type = None + + if arg_type: + table.add_row( + f"{arg_name} [{arg_type}]", + str(display_value), + ) + else: + table.add_row(arg_name, str(display_value)) + console.print(table) return True, f"Displayed info for crowdloan #{crowdloan_id}" From 3260dec3fc358ab2d47d513e4d47ae3ee9007a51 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 15:57:57 -0700 Subject: [PATCH 29/69] update cli --- bittensor_cli/cli.py | 97 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c4b60f48..95ca015e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1156,6 +1156,9 @@ def __init__(self): self.crowd_app.command( "withdraw", rich_help_panel=HELP_PANELS["CROWD"]["PARTICIPANT"] )(self.crowd_withdraw) + self.crowd_app.command( + "finalize", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] + )(self.crowd_finalize) # Liquidity self.app.add_typer( @@ -7359,7 +7362,27 @@ def crowd_create( None, "--target-address", "--target", - help="Optional target SS58 address to receive the raised funds.", + help="Optional target SS58 address to receive the raised funds (for fundraising type).", + ), + subnet_lease: Optional[bool] = typer.Option( + None, + "--subnet-lease/--fundraising", + help="Create a subnet leasing crowdloan (True) or general fundraising (False).", + ), + emissions_share: Optional[int] = typer.Option( + None, + "--emissions-share", + "--emissions", + help="Percentage of emissions for contributors (0-100) for subnet leasing.", + min=0, + max=100, + ), + lease_end_block: Optional[int] = typer.Option( + None, + "--lease-end-block", + "--lease-end", + help="Block number when subnet lease ends (omit for perpetual lease).", + min=1, ), prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, @@ -7368,7 +7391,23 @@ def crowd_create( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Start a new crowdloan campaign to raise funds for a subnet.""" + """Start a new crowdloan campaign for fundraising or subnet leasing. + + 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 + + EXAMPLES + + General fundraising: + [green]$[/green] btcli crowd create --deposit 10 --cap 1000 --target-address 5D... + + Subnet leasing with 30% emissions for contributors: + [green]$[/green] btcli crowd create --subnet-lease --emissions-share 30 + + Subnet lease ending at block 500000: + [green]$[/green] btcli crowd create --subnet-lease --emissions-share 25 --lease-end-block 500000 + """ self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( @@ -7388,6 +7427,9 @@ def crowd_create( cap_tao=cap, duration_blocks=duration, target_address=target_address, + subnet_lease=subnet_lease, + emissions_share=emissions_share, + lease_end_block=lease_end_block, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, @@ -7512,6 +7554,57 @@ def crowd_withdraw( ) ) + def crowd_finalize( + self, + crowdloan_id: Optional[int] = typer.Option( + None, + "--crowdloan-id", + "--crowdloan_id", + "--id", + help="The ID of the crowdloan to finalize", + ), + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Finalize a successful crowdloan that has reached its cap. + + Only the creator can finalize. This will transfer funds to the target + address (if specified) and execute any attached call (e.g., subnet creation). + """ + 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, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + return self._run_command( + create_crowdloan.finalize_crowdloan( + subtensor=self.initialize_chain(network), + wallet=wallet, + crowdloan_id=crowdloan_id, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + ) + @staticmethod def convert( from_rao: Optional[str] = typer.Option( From 44a9e0150c12fba9ef19d5db435aa95551f4bd38 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 15:58:28 -0700 Subject: [PATCH 30/69] wip --- bittensor_cli/src/commands/crowd/create.py | 355 ++++++++++++++++++--- 1 file changed, 318 insertions(+), 37 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 1ea5f2f2..94b2e71f 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -6,6 +6,7 @@ from rich.table import Table, Column, box from bittensor_cli.src import COLORS +from bittensor_cli.src.commands.crowd.view import show_crowdloan_details from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( @@ -13,7 +14,9 @@ console, err_console, is_valid_ss58_address, + print_error, unlock_key, + print_extrinsic_id, ) @@ -25,6 +28,9 @@ async def create_crowdloan( cap_tao: Optional[int], duration_blocks: Optional[int], target_address: Optional[str], + subnet_lease: Optional[bool], + emissions_share: Optional[int], + lease_end_block: Optional[int], wait_for_inclusion: bool, wait_for_finalization: bool, prompt: bool, @@ -40,6 +46,37 @@ async def create_crowdloan( err_console.print(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message + crowdloan_type = None + if subnet_lease is not None: + crowdloan_type = "subnet" if subnet_lease else "fundraising" + elif prompt: + type_choice = IntPrompt.ask( + "\n[bold cyan]What type of crowdloan would you like to create?[/bold cyan]\n" + "[cyan][1][/cyan] General Fundraising (funds go to address)\n" + "[cyan][2][/cyan] Subnet Leasing (create new subnet)", + choices=["1", "2"] + ) + crowdloan_type = "subnet" if type_choice == 2 else "fundraising" + + if crowdloan_type == "subnet": + current_burn_cost = await subtensor.burn_cost() + console.print( + "\n[magenta]Subnet Lease Crowdloan Selected[/magenta]\n" + " • A new subnet will be created when the crowdloan is finalized\n" + " • Contributors will receive emissions as dividends\n" + " • You will become the subnet operator\n" + f" • [yellow]Note: Ensure cap covers subnet registration cost (currently {current_burn_cost.tao:,.2f} TAO)[/yellow]\n" + ) + else: + console.print( + "\n[cyan]General Fundraising Crowdloan Selected[/cyan]\n" + " • Funds will be transferred to a target address when finalized\n" + " • Contributors can withdraw if the cap is not reached\n" + ) + else: + print_error("Crowdloan type not specified and no prompt provided.") + return False, "Crowdloan type not specified and no prompt provided." + async def _get_constant(constant_name: str) -> int: result = await subtensor.substrate.get_constant( module_name="Crowdloan", @@ -165,26 +202,62 @@ async def _get_constant(constant_name: str) -> int: duration = duration_value break - if target_address and target_address.strip(): - target_address = target_address.strip() - if not is_valid_ss58_address(target_address): + current_block = await subtensor.substrate.get_block_number(None) + call_to_attach = None + + if crowdloan_type == "subnet": + target_address = None + + if emissions_share is None: + emissions_share = IntPrompt.ask( + "Enter emissions share percentage for contributors [blue](0-100)[/blue]" + ) + + if not 0 <= emissions_share <= 100: + err_console.print( + f"[red]Emissions share must be between 0 and 100, got {emissions_share}[/red]" + ) + return False, "Invalid emissions share percentage." + + if lease_end_block is None: + lease_perpetual = Confirm.ask( + "Should the subnet lease be perpetual?", + default=True, + ) + if not lease_perpetual: + lease_end_block = IntPrompt.ask( + f"Enter the block number when the lease should end. Current block is [bold]{current_block}[/bold]." + ) + register_lease_call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="register_leased_network", + call_params={ + "emissions_share": emissions_share, + "end_block": None if lease_perpetual else lease_end_block, + } + ) + call_to_attach = register_lease_call + else: + if target_address: + target_address = target_address.strip() + if not is_valid_ss58_address(target_address): + err_console.print( + f"[red]Invalid target SS58 address provided: {target_address}[/red]" + ) + return False, "Invalid target SS58 address provided." + elif prompt: + target_input = Prompt.ask( + "Enter a target SS58 address (leave blank for none)", + ) + target_address = target_input.strip() or None + + if target_address and not is_valid_ss58_address(target_address): err_console.print( f"[red]Invalid target SS58 address provided: {target_address}[/red]" ) return False, "Invalid target SS58 address provided." - elif prompt: - target_input = Prompt.ask( - "Enter a target SS58 address (leave blank for none)", - default="", - show_default=False, - ) - target_address = target_input.strip() or None - if target_address and not is_valid_ss58_address(target_address): - err_console.print( - f"[red]Invalid target SS58 address provided: {target_address}[/red]" - ) - return False, "Invalid target SS58 address provided." + call_to_attach = None creator_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) if deposit > creator_balance: @@ -194,7 +267,6 @@ async def _get_constant(constant_name: str) -> int: ) return False, "Insufficient balance to cover the deposit." - current_block = await subtensor.substrate.get_block_number(None) end_block = current_block + duration call = await subtensor.substrate.compose_call( @@ -205,7 +277,7 @@ async def _get_constant(constant_name: str) -> int: "min_contribution": min_contribution.rao, "cap": cap.rao, "end": end_block, - "call": None, + "call": call_to_attach, "target_address": target_address, }, ) @@ -214,11 +286,6 @@ async def _get_constant(constant_name: str) -> int: if prompt: duration_text = blocks_to_duration(duration) - target_text = ( - target_address - if target_address - else f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]" - ) table = Table( Column("[bold white]Field", style=COLORS.G.SUBHEAD), @@ -234,6 +301,22 @@ async def _get_constant(constant_name: str) -> int: border_style="bright_black", ) + if crowdloan_type == "subnet": + table.add_row("Type", "[magenta]Subnet Leasing[/magenta]") + table.add_row("Emissions Share", f"[cyan]{emissions_share}%[/cyan] for contributors") + if lease_end_block: + table.add_row("Lease Ends", f"Block {lease_end_block}") + else: + table.add_row("Lease Duration", "[green]Perpetual[/green]") + else: + table.add_row("Type", "[cyan]General Fundraising[/cyan]") + target_text = ( + target_address + if target_address + else f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]" + ) + table.add_row("Target address", target_text) + table.add_row("Deposit", f"[{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]") table.add_row( "Min contribution", f"[{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]" @@ -241,7 +324,6 @@ async def _get_constant(constant_name: str) -> int: table.add_row("Cap", f"[{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]") table.add_row("Duration", f"[bold]{duration}[/bold] blocks (~{duration_text})") table.add_row("Ends at block", f"[bold]{end_block}[/bold]") - table.add_row("Target address", target_text) table.add_row( "Estimated fee", f"[{COLORS.P.TAO}]{extrinsic_fee}[/{COLORS.P.TAO}]" ) @@ -254,13 +336,13 @@ async def _get_constant(constant_name: str) -> int: success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + wait_for_inclusion=False, + wait_for_finalization=False, ) extrinsic_id = None - if extrinsic_receipt: - extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + # if extrinsic_receipt: + # extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() if not success: err_console.print( @@ -268,17 +350,216 @@ async def _get_constant(constant_name: str) -> int: ) return False, error_message or "Failed to create crowdloan." - message = "Crowdloan created successfully." - console.print( - f"\n:white_check_mark: [green]{message}[/green]\n" - f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" - f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" - f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" - f" Ends at block: [bold]{end_block}[/bold]" - ) - if target_address: - console.print(f" Target address: {target_address}") + if crowdloan_type == "subnet": + message = "Subnet lease crowdloan created successfully." + console.print( + f"\n:white_check_mark: [green]{message}[/green]\n" + f" Type: [magenta]Subnet Leasing[/magenta]\n" + f" Emissions Share: [cyan]{emissions_share}%[/cyan]\n" + f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" + f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" + f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" + f" Ends at block: [bold]{end_block}[/bold]" + ) + if lease_end_block: + console.print(f" Lease ends at block: [bold]{lease_end_block}[/bold]") + else: + console.print(" Lease: [green]Perpetual[/green]") + else: + message = "Fundraising crowdloan created successfully." + console.print( + f"\n:white_check_mark: [green]{message}[/green]\n" + f" Type: [cyan]General Fundraising[/cyan]\n" + f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" + f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" + f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" + f" Ends at block: [bold]{end_block}[/bold]" + ) + if target_address: + console.print(f" Target address: {target_address}") + if extrinsic_id: console.print(f" Extrinsic ID: [bold]{extrinsic_id}[/bold]") return True, message + + +async def finalize_crowdloan( + subtensor: SubtensorInterface, + wallet: Wallet, + crowdloan_id: int, + wait_for_inclusion: bool, + wait_for_finalization: bool, + prompt: bool, +) -> tuple[bool, str]: + """ + Finalize a successful crowdloan that has reached its cap. + + Only the creator can finalize a crowdloan. Finalization will: + - Transfer funds to the target address (if specified) + - Execute the attached call (if any, e.g., subnet creation) + - Mark the crowdloan as finalized + + Args: + subtensor: SubtensorInterface instance for blockchain interaction + wallet: Wallet instance containing the user's keys + crowdloan_id: The ID of the crowdloan to finalize + wait_for_inclusion: Whether to wait for transaction inclusion + wait_for_finalization: Whether to wait for transaction finalization + prompt: Whether to prompt for user confirmation + + Returns: + Tuple of (success, message) indicating the result + """ + + crowdloan, current_block = await asyncio.gather( + subtensor.get_single_crowdloan(crowdloan_id), + subtensor.substrate.get_block_number(None), + ) + + if not crowdloan: + err_console.print(f"[red]Crowdloan #{crowdloan_id} does not exist.[/red]") + return False, f"Crowdloan #{crowdloan_id} does not exist." + + if wallet.coldkeypub.ss58_address != crowdloan.creator: + err_console.print( + f"[red]Only the creator can finalize a crowdloan. " + f"Creator: {crowdloan.creator}[/red]" + ) + return False, "Only the creator can finalize a crowdloan." + + if crowdloan.finalized: + err_console.print(f"[red]Crowdloan #{crowdloan_id} is already finalized.[/red]") + return False, "Crowdloan is already finalized." + + if crowdloan.raised < crowdloan.cap: + err_console.print( + f"[red]Crowdloan #{crowdloan_id} has not reached its cap.\n" + f"Raised: {crowdloan.raised}, Cap: {crowdloan.cap}\n" + f"Still needed: {Balance.from_rao(crowdloan.cap.rao - crowdloan.raised.rao)}[/red]" + ) + return False, "Crowdloan has not reached its cap." + + call = await subtensor.substrate.compose_call( + call_module="Crowdloan", + call_function="finalize", + call_params={ + "crowdloan_id": crowdloan_id, + }, + ) + extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + + await show_crowdloan_details( + subtensor=subtensor, + crowdloan_id=crowdloan_id, + wallet=wallet, + verbose=False, + crowdloan=crowdloan, + current_block=current_block, + ) + + if prompt: + console.print() + table = Table( + Column("[bold white]Field", style=COLORS.G.SUBHEAD), + Column("[bold white]Value", style=COLORS.G.TEMPO), + title="\n[bold cyan]Crowdloan Finalization Summary[/bold cyan]", + show_footer=False, + show_header=False, + width=None, + pad_edge=False, + box=box.SIMPLE, + show_edge=True, + border_style="bright_black", + ) + + table.add_row("Crowdloan ID", str(crowdloan_id)) + table.add_row("Status", "[green]Ready to Finalize[/green]") + table.add_row( + "Total Raised", f"[{COLORS.S.AMOUNT}]{crowdloan.raised}[/{COLORS.S.AMOUNT}]" + ) + table.add_row("Contributors", str(crowdloan.contributors_count)) + + if crowdloan.target_address: + table.add_row( + "Funds Will Go To", + f"[{COLORS.G.SUBHEAD_EX_1}]{crowdloan.target_address}[/{COLORS.G.SUBHEAD_EX_1}]", + ) + else: + table.add_row( + "Funds Will Go To", + "[yellow]Funds Account (manual transfer required)[/yellow]", + ) + + if crowdloan.has_call: + table.add_row( + "Call to Execute", "[yellow]Yes (e.g., subnet registration)[/yellow]" + ) + else: + table.add_row("Call to Execute", "[dim]None[/dim]") + + table.add_row("Transaction Fee", str(extrinsic_fee)) + + table.add_section() + table.add_row( + "[bold red]WARNING[/bold red]", + "[yellow]This action is IRREVERSIBLE![/yellow]", + ) + + console.print(table) + + console.print( + "\n[bold yellow]Important:[/bold yellow]\n" + "• Finalization will transfer all raised funds\n" + "• Any attached call will be executed immediately\n" + "• This action cannot be undone\n" + ) + + if not Confirm.ask("\nProceed with finalization?"): + console.print("[yellow]Finalization cancelled.[/yellow]") + return False, "Finalization cancelled by user." + + unlock_status = unlock_key(wallet) + if not unlock_status.success: + err_console.print(f"[red]{unlock_status.message}[/red]") + return False, unlock_status.message + + success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not success: + err_console.print( + f"[red]Failed to finalize: {error_message or 'Unknown error'}[/red]" + ) + return False, error_message or "Failed to finalize crowdloan." + + console.print( + f"\n[dark_sea_green3]Successfully finalized crowdloan #{crowdloan_id}![/dark_sea_green3]\n" + ) + + console.print( + f"[bold]Finalization Complete:[/bold]\n" + f" • Total Raised: [{COLORS.S.AMOUNT}]{crowdloan.raised}[/{COLORS.S.AMOUNT}]\n" + f" • Contributors: {crowdloan.contributors_count}" + ) + + if crowdloan.target_address: + console.print( + f" • Funds transferred to: [{COLORS.G.SUBHEAD_EX_1}]{crowdloan.target_address}[/{COLORS.G.SUBHEAD_EX_1}]" + ) + else: + console.print( + f" • Funds remain in: [{COLORS.G.SUBHEAD_EX_2}]{crowdloan.funds_account}[/{COLORS.G.SUBHEAD_EX_2}]" + ) + + if crowdloan.has_call: + console.print(" • [yellow]Associated call has been executed[/yellow]") + + if extrinsic_receipt: + await print_extrinsic_id(extrinsic_receipt) + + return True, "Successfully finalized crowdloan." From 0b7d875b2c3b98e8a3957ee6c32e8cc30b74eaf7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 17:27:29 -0700 Subject: [PATCH 31/69] add helper --- bittensor_cli/src/commands/crowd/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 bittensor_cli/src/commands/crowd/utils.py diff --git a/bittensor_cli/src/commands/crowd/utils.py b/bittensor_cli/src/commands/crowd/utils.py new file mode 100644 index 00000000..c405d882 --- /dev/null +++ b/bittensor_cli/src/commands/crowd/utils.py @@ -0,0 +1,9 @@ +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def get_constant(subtensor: SubtensorInterface, constant_name: str) -> int: + result = await subtensor.substrate.get_constant( + module_name="Crowdloan", + constant_name=constant_name, + ) + return getattr(result, "value", result) From fd162e9a39e0cd22397129f50e08d05931cd3e71 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 17:30:53 -0700 Subject: [PATCH 32/69] wip --- bittensor_cli/src/commands/crowd/update.py | 257 +++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 bittensor_cli/src/commands/crowd/update.py diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py new file mode 100644 index 00000000..8d66a68e --- /dev/null +++ b/bittensor_cli/src/commands/crowd/update.py @@ -0,0 +1,257 @@ +import asyncio +from typing import Optional + +from bittensor_wallet import Wallet +from rich.prompt import Confirm, IntPrompt, FloatPrompt +from rich.table import Table, Column, box + +from bittensor_cli.src import COLORS +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.utils import ( + blocks_to_duration, + console, + print_error, + unlock_key, + print_extrinsic_id, +) +from bittensor_cli.src.commands.crowd.view import show_crowdloan_details +from bittensor_cli.src.commands.crowd.utils import get_constant + + +async def update_crowdloan( + subtensor: SubtensorInterface, + wallet: Wallet, + crowdloan_id: int, + min_contribution: Optional[Balance] = None, + end: Optional[int] = None, + cap: Optional[Balance] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = True, +) -> tuple[bool, str]: + """Update parameters of a non-finalized crowdloan. + + Args: + subtensor: SubtensorInterface object for chain interaction + wallet: Wallet object containing coldkey (must be creator) + crowdloan_id: ID of the crowdloan to update + min_contribution: New minimum contribution in TAO (None to prompt) + end: New end block (None to prompt) + cap: New cap in TAO (None to prompt) + wait_for_inclusion: Wait for transaction inclusion + wait_for_finalization: Wait for transaction finalization + prompt: Whether to prompt for values + + Returns: + tuple[bool, str]: Success status and message + """ + + ( + crowdloan, + current_block, + absolute_min_rao, + min_duration, + max_duration, + ) = await asyncio.gather( + subtensor.get_single_crowdloan(crowdloan_id), + subtensor.substrate.get_block_number(None), + get_constant(subtensor, "AbsoluteMinimumContribution"), + get_constant(subtensor, "MinimumBlockDuration"), + get_constant(subtensor, "MaximumBlockDuration"), + ) + absolute_min = Balance.from_rao(absolute_min_rao) + + if not crowdloan: + print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") + return False, f"Crowdloan #{crowdloan_id} not found." + + if crowdloan.finalized: + print_error( + f"[red]Crowdloan #{crowdloan_id} is already finalized and cannot be updated.[/red]" + ) + return False, f"Crowdloan #{crowdloan_id} is already finalized." + + creator_address = wallet.coldkeypub.ss58_address + if creator_address != crowdloan.creator: + print_error( + f"[red]Only the creator can update this crowdloan.[/red]\n" + f"Creator: [blue]{crowdloan.creator}[/blue]\n" + f"Your address: [blue]{creator_address}[/blue]" + ) + return False, "Only the creator can update this crowdloan." + + await show_crowdloan_details( + subtensor=subtensor, + crowdloan_id=crowdloan_id, + wallet=wallet, + verbose=False, + crowdloan=crowdloan, + current_block=current_block, + ) + + if all(x is None for x in [min_contribution, end, cap]) and prompt: + console.print( + f"\n[bold cyan]What would you like to update for Crowdloan #{crowdloan_id}?[/bold cyan]\n" + ) + time_left = blocks_to_duration(crowdloan.end - current_block) + choice = IntPrompt.ask( + f"[cyan][1][/cyan] Minimum Contribution (current: [yellow]{crowdloan.min_contribution}[/yellow])\n" + f"[cyan][2][/cyan] End Block (current: [yellow]block {crowdloan.end:,}[/yellow], {time_left} remaining)\n" + f"[cyan][3][/cyan] Cap (current: [yellow]{crowdloan.cap}[/yellow])\n" + f"[cyan][4][/cyan] Cancel\n\n" + f"Enter your choice", + choices=["1", "2", "3", "4"], + default=4, + ) + + if choice == 4: + console.print("[yellow]Update cancelled.[/yellow]") + return False, "Update cancelled by user." + + if choice == 1: + console.print( + f"\n[cyan]Update Minimum Contribution[/cyan]" + f"\n • Current: [yellow]{crowdloan.min_contribution}[/yellow]" + f"\n • Absolute minimum: [dim]{absolute_min}[/dim]\n" + ) + + while True: + new_value = FloatPrompt.ask( + "Enter new minimum contribution (TAO)", + default=float(crowdloan.min_contribution.tao), + ) + candidate = Balance.from_tao(new_value) + if candidate.rao < absolute_min.rao: + print_error( + f"[red]Minimum contribution must be at least {absolute_min}. Try again.[/red]" + ) + continue + min_contribution = candidate + break + + elif choice == 2: + min_end_block = current_block + min_duration + max_end_block = current_block + max_duration + duration_remaining = blocks_to_duration(crowdloan.end - current_block) + console.print( + f"\n[cyan]Update End Block[/cyan]" + f"\n • Current: [yellow]block {crowdloan.end:,}[/yellow] ({duration_remaining} remaining)" + f"\n • Current block: [dim]{current_block:,}[/dim]" + f"\n • Valid range: [dim]{min_end_block:,} - {max_end_block:,}[/dim]" + f"\n • Duration range: [dim]{blocks_to_duration(min_duration)} - {blocks_to_duration(max_duration)}[/dim]\n" + ) + + while True: + candidate_end = IntPrompt.ask( + "Enter new end block", + default=crowdloan.end, + ) + + if candidate_end <= current_block: + print_error( + f"[red]End block must be after current block ({current_block:,}). Try again.[/red]" + ) + continue + + duration = candidate_end - current_block + if duration < min_duration: + duration_range = f"[dim]{min_end_block} - {blocks_to_duration(min_duration)}[/dim]" + print_error( + f"[red]Duration is too short. Minimum: {duration_range}. Try again.[/red]" + ) + continue + if duration > max_duration: + duration_range = f"[dim]{max_end_block} - {blocks_to_duration(max_duration)}[/dim]" + print_error( + f"[red]Duration is too long. Maximum: {duration_range}. Try again.[/red]" + ) + continue + + end = candidate_end + break + + elif choice == 3: + console.print( + f"\n[cyan]Update Cap[/cyan]" + f"\n • Current cap: [yellow]{crowdloan.cap}[/yellow]" + f"\n • Already raised: [green]{crowdloan.raised}[/green]" + f"\n • Remaining to raise: [dim]{(crowdloan.cap.rao - crowdloan.raised.rao) / 1e9:.9f} TAO[/dim]" + f"\n • New cap must be >= raised amount\n" + ) + + while True: + new_value = FloatPrompt.ask( + "Enter new cap (TAO)", + default=float(crowdloan.cap.tao), + ) + candidate_cap = Balance.from_tao(new_value) + if candidate_cap.rao < crowdloan.raised.rao: + print_error( + f"[red]Cap must be >= amount already raised ({crowdloan.raised}). Try again.[/red]" + ) + continue + cap = candidate_cap + break + + value: Optional[Balance | int] = None + call_function: Optional[str] = None + param_name: Optional[str] = None + update_type: Optional[str] = None + + if min_contribution is not None: + value = min_contribution + call_function = "update_min_contribution" + param_name = "new_min_contribution" + update_type = "Minimum Contribution" + elif cap is not None: + value = cap + call_function = "update_cap" + param_name = "new_cap" + update_type = "Cap" + elif end is not None: + value = end + call_function = "update_end" + param_name = "new_end" + update_type = "End Block" + + if call_function is None or value is None or param_name is None: + print_error("[red]No update parameter specified.[/red]") + return False, "No update parameter specified." + + # Validation + if call_function == "update_min_contribution": + if value.rao < absolute_min.rao: + print_error( + f"[red]Minimum contribution ({value}) must be at least {absolute_min}.[/red]" + ) + return False, f"Minimum contribution must be at least {absolute_min}." + + elif call_function == "update_end": + if value <= current_block: + print_error( + f"[red]End block ({value:,}) must be after current block ({current_block:,}).[/red]" + ) + return False, "End block must be in the future." + + block_duration = value - current_block + if block_duration < min_duration: + print_error( + f"[red]Duration ({blocks_to_duration(block_duration)}) is too short. " + f"Minimum: [dim]{min_end_block} - {blocks_to_duration(min_duration)}[/dim][/red]" + ) + return False, "Block duration too short." + + if block_duration > max_duration: + print_error( + f"[red]Duration ({blocks_to_duration(block_duration)}) is too long. " + f"Maximum: [dim]{max_end_block} - {blocks_to_duration(max_duration)}[/dim][/red]" + ) + return False, "Block duration too long." + + elif call_function == "update_cap": + if value < crowdloan.raised: + print_error( + f"[red]New cap ({value}) must be at least the amount already raised ({crowdloan.raised}).[/red]" + ) + return False, "Cap must be >= raised amount." From 71134ddaf66958f4a7b7fd26ca2f0581058c1cc9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 17:31:05 -0700 Subject: [PATCH 33/69] add update --- bittensor_cli/src/commands/crowd/update.py | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py index 8d66a68e..93c5aa0e 100644 --- a/bittensor_cli/src/commands/crowd/update.py +++ b/bittensor_cli/src/commands/crowd/update.py @@ -255,3 +255,89 @@ async def update_crowdloan( f"[red]New cap ({value}) must be at least the amount already raised ({crowdloan.raised}).[/red]" ) return False, "Cap must be >= raised amount." + + # Update summary + table = Table( + Column("[bold white]Parameter", style=COLORS.G.SUBHEAD), + Column("[bold white]Current Value", style=COLORS.G.TEMPO), + Column("[bold white]New Value", style=COLORS.G.TEMPO), + title="\n[bold cyan]Update Summary[/bold cyan]", + show_footer=False, + width=None, + pad_edge=False, + box=box.SIMPLE, + show_edge=True, + border_style="bright_black", + ) + + if call_function == "update_min_contribution": + table.add_row( + "Minimum Contribution", str(crowdloan.min_contribution), str(value) + ) + elif call_function == "update_end": + table.add_row( + "End Block", + f"{crowdloan.end:,} ({blocks_to_duration(crowdloan.end - current_block)} remaining)", + f"{value:,} ({blocks_to_duration(value - current_block)} remaining)", + ) + elif call_function == "update_cap": + table.add_row("Cap", str(crowdloan.cap), str(value)) + + console.print(table) + + if prompt and not Confirm.ask( + f"\n[bold]Proceed with updating {update_type}?[/bold]", default=False + ): + console.print("[yellow]Update cancelled.[/yellow]") + return False, "Update cancelled by user." + + unlock_status = unlock_key(wallet) + if not unlock_status.success: + print_error(f"[red]{unlock_status.message}[/red]") + return False, unlock_status.message + + if call_function == "update_min_contribution": + value = value.rao + + with console.status(f":satellite: Updating {update_type}...", spinner="earth"): + call = await subtensor.substrate.compose_call( + call_module="Crowdloan", + call_function=call_function, + call_params={"crowdloan_id": crowdloan_id, param_name: value}, + ) + + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + + with console.status( + ":satellite: Submitting update transaction...", spinner="aesthetic" + ): + response = await subtensor.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + console.print( + ":white_heavy_check_mark: [green]Update transaction submitted.[/green]" + ) + return True, "Update transaction submitted." + + await response.process_events() + + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed to update {update_type}.[/red]\n" + f"{response.error_message}" + ) + return False, response.error_message.get("name", "Unknown error") + + console.print( + f":white_heavy_check_mark: [green]{update_type} updated successfully![/green]\n" + f"Crowdloan #{crowdloan_id} has been updated." + ) + await print_extrinsic_id(response) + + return True, f"{update_type} updated successfully." From 697458774f32f1bb0fbf5f46c271fb638a3905f5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 17:31:17 -0700 Subject: [PATCH 34/69] use util --- bittensor_cli/src/commands/crowd/create.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 94b2e71f..984fbd75 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -9,6 +9,7 @@ from bittensor_cli.src.commands.crowd.view import show_crowdloan_details 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_constant from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, @@ -77,23 +78,16 @@ async def create_crowdloan( print_error("Crowdloan type not specified and no prompt provided.") return False, "Crowdloan type not specified and no prompt provided." - async def _get_constant(constant_name: str) -> int: - result = await subtensor.substrate.get_constant( - module_name="Crowdloan", - constant_name=constant_name, - ) - return getattr(result, "value", result) - ( minimum_deposit_raw, min_contribution_raw, min_duration, max_duration, ) = await asyncio.gather( - _get_constant("MinimumDeposit"), - _get_constant("AbsoluteMinimumContribution"), - _get_constant("MinimumBlockDuration"), - _get_constant("MaximumBlockDuration"), + get_constant(subtensor, "MinimumDeposit"), + get_constant(subtensor, "AbsoluteMinimumContribution"), + get_constant(subtensor, "MinimumBlockDuration"), + get_constant(subtensor, "MaximumBlockDuration"), ) minimum_deposit = Balance.from_rao(minimum_deposit_raw) From b87685464a6f772847c847cffcb68158462d98e8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 20:09:00 -0700 Subject: [PATCH 35/69] wip --- bittensor_cli/src/commands/crowd/dissolve.py | 90 ++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 bittensor_cli/src/commands/crowd/dissolve.py diff --git a/bittensor_cli/src/commands/crowd/dissolve.py b/bittensor_cli/src/commands/crowd/dissolve.py new file mode 100644 index 00000000..ca757407 --- /dev/null +++ b/bittensor_cli/src/commands/crowd/dissolve.py @@ -0,0 +1,90 @@ +import asyncio + +from bittensor_wallet import Wallet +from rich.prompt import Confirm +from rich.table import Column, Table, box + +from bittensor_cli.src import COLORS +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.commands.crowd.view import show_crowdloan_details +from bittensor_cli.src.bittensor.utils import ( + blocks_to_duration, + console, + print_extrinsic_id, + print_error, + unlock_key, + format_error_message, +) + + +async def dissolve_crowdloan( + subtensor: SubtensorInterface, + wallet: Wallet, + crowdloan_id: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = True, +) -> tuple[bool, str]: + """Dissolve a non-finalized crowdloan after refunding contributors. + + The creator can reclaim their deposit once every other contribution has been + refunded (i.e., the raised amount equals the creator's contribution). + + Args: + subtensor: SubtensorInterface object for chain interaction. + wallet: Wallet object containing the creator's coldkey. + crowdloan_id: ID of the crowdloan to dissolve. + wait_for_inclusion: Wait for transaction inclusion. + wait_for_finalization: Wait for transaction finalization. + prompt: Whether to prompt for confirmation. + + Returns: + tuple[bool, str]: Success status and message. + """ + + creator_ss58 = wallet.coldkeypub.ss58_address + + crowdloan, current_block = await asyncio.gather( + subtensor.get_single_crowdloan(crowdloan_id), + subtensor.substrate.get_block_number(None), + ) + + if not crowdloan: + print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") + return False, f"Crowdloan #{crowdloan_id} not found." + + if crowdloan.finalized: + print_error( + f"[red]Crowdloan #{crowdloan_id} is already finalized and cannot be dissolved.[/red]" + ) + return False, f"Crowdloan #{crowdloan_id} is finalized." + + if creator_ss58 != crowdloan.creator: + print_error( + f"[red]Only the creator can dissolve this crowdloan.[/red]\n" + f"Creator: [blue]{crowdloan.creator}[/blue]\n" + f"Your address: [blue]{creator_ss58}[/blue]" + ) + return False, "Only the creator can dissolve this crowdloan." + + creator_contribution = await subtensor.get_crowdloan_contribution( + crowdloan_id, crowdloan.creator + ) + + if creator_contribution != crowdloan.raised: + print_error( + f"[red]Crowdloan still holds funds from other contributors.[/red]\n" + f"Raised amount: [yellow]{crowdloan.raised}[/yellow]\n" + f"Creator's contribution: [yellow]{creator_contribution}[/yellow]\n" + "Run [cyan]btcli crowd refund[/cyan] until only the creator's funds remain." + ) + return False, "Crowdloan not ready to dissolve." + + await show_crowdloan_details( + subtensor=subtensor, + crowdloan_id=crowdloan_id, + wallet=wallet, + verbose=False, + crowdloan=crowdloan, + current_block=current_block, + ) From 29506fdbe150237535666eb986949ae5508ee47c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 20:09:08 -0700 Subject: [PATCH 36/69] dissolve_crowdloan --- bittensor_cli/src/commands/crowd/dissolve.py | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/bittensor_cli/src/commands/crowd/dissolve.py b/bittensor_cli/src/commands/crowd/dissolve.py index ca757407..8a09da49 100644 --- a/bittensor_cli/src/commands/crowd/dissolve.py +++ b/bittensor_cli/src/commands/crowd/dissolve.py @@ -88,3 +88,73 @@ async def dissolve_crowdloan( crowdloan=crowdloan, current_block=current_block, ) + + summary = Table( + Column("Field", style=COLORS.G.SUBHEAD), + Column("Value", style=COLORS.G.TEMPO), + box=box.SIMPLE, + show_header=False, + ) + summary.add_row("Crowdloan ID", f"#{crowdloan_id}") + summary.add_row("Raised", str(crowdloan.raised)) + summary.add_row("Creator Contribution", str(creator_contribution)) + summary.add_row( + "Remaining Contributors", + str(max(0, crowdloan.contributors_count - 1)), + ) + time_remaining = crowdloan.end - current_block + summary.add_row( + "Time Remaining", + blocks_to_duration(time_remaining) if time_remaining > 0 else "Ended", + ) + + console.print("\n[bold cyan]Crowdloan Dissolution Summary[/bold cyan]") + console.print(summary) + + if prompt and not Confirm.ask( + f"\n[bold]Proceed with dissolving crowdloan #{crowdloan_id}?[/bold]", + default=False, + ): + console.print("[yellow]Dissolution cancelled.[/yellow]") + return False, "Dissolution cancelled by user." + + unlock_status = unlock_key(wallet) + if not unlock_status.success: + print_error(f"[red]{unlock_status.message}[/red]") + return False, unlock_status.message + + with console.status( + ":satellite: Submitting dissolve transaction...", spinner="aesthetic" + ): + call = await subtensor.substrate.compose_call( + call_module="Crowdloan", + call_function="dissolve", + call_params={"crowdloan_id": crowdloan_id}, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.coldkey, + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + console.print("[green]Dissolve transaction submitted.[/green]") + return True, "Dissolve transaction submitted." + + await response.process_events() + + if not await response.is_success: + print_error( + f"[red]Failed to dissolve crowdloan.[/red]\n{format_error_message(await response.error_message)}" + ) + return False, format_error_message(await response.error_message) + + await print_extrinsic_id(response) + + console.print("[green]Crowdloan dissolved successfully![/green]") + + return True, "Crowdloan dissolved successfully." From bb334eafadc850bacf0c6637ca52ffda5f99216c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 20:10:01 -0700 Subject: [PATCH 37/69] wip --- bittensor_cli/src/commands/crowd/refund.py | 123 +++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 bittensor_cli/src/commands/crowd/refund.py diff --git a/bittensor_cli/src/commands/crowd/refund.py b/bittensor_cli/src/commands/crowd/refund.py new file mode 100644 index 00000000..e44678a5 --- /dev/null +++ b/bittensor_cli/src/commands/crowd/refund.py @@ -0,0 +1,123 @@ +import asyncio + +from bittensor_wallet import Wallet +from rich.prompt import Confirm +from rich.table import Table, Column, box + +from bittensor_cli.src import COLORS +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.utils import ( + blocks_to_duration, + console, + print_extrinsic_id, + print_error, + unlock_key, +) +from bittensor_cli.src.commands.crowd.view import show_crowdloan_details +from bittensor_cli.src.commands.crowd.utils import get_constant + + +async def refund_crowdloan( + subtensor: SubtensorInterface, + wallet: Wallet, + crowdloan_id: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = True, +) -> tuple[bool, str]: + """Refund contributors of a non-finalized crowdloan. + + This extrinsic refunds all contributors (excluding the creator) up to the + RefundContributorsLimit. If there are more contributors than the limit, + this call may need to be executed multiple times until all contributors + are refunded. + + Anyone can call this function - it does not need to be the creator. + + Args: + subtensor: SubtensorInterface object for chain interaction + wallet: Wallet object containing coldkey (any wallet can call this) + crowdloan_id: ID of the crowdloan to refund + wait_for_inclusion: Wait for transaction inclusion + wait_for_finalization: Wait for transaction finalization + prompt: Whether to prompt for confirmation + + Returns: + tuple[bool, str]: Success status and message + """ + crowdloan, current_block = await asyncio.gather( + subtensor.get_single_crowdloan(crowdloan_id), + subtensor.substrate.get_block_number(None), + ) + + if not crowdloan: + print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") + return False, f"Crowdloan #{crowdloan_id} not found." + + if crowdloan.finalized: + print_error( + f"[red]Crowdloan #{crowdloan_id} is already finalized. " + "Finalized crowdloans cannot be refunded.[/red]" + ) + return False, f"Crowdloan #{crowdloan_id} is already finalized." + if crowdloan.end > current_block: + print_error( + f"[red]Crowdloan #{crowdloan_id} is not yet ended. " + f"End block: [cyan]{crowdloan.end:,}[/cyan] ([dim]{blocks_to_duration(crowdloan.end - current_block)} remaining[/dim])[/red]" + ) + return False, f"Crowdloan #{crowdloan_id} is not yet ended." + + await show_crowdloan_details( + subtensor=subtensor, + crowdloan_id=crowdloan_id, + wallet=wallet, + verbose=False, + crowdloan=crowdloan, + current_block=current_block, + ) + + refund_limit = await get_constant(subtensor, "RefundContributorsLimit") + + console.print("\n[bold cyan]Crowdloan Refund Information[/bold cyan]\n") + + info_table = Table( + Column("[bold white]Property", style=COLORS.G.SUBHEAD), + Column("[bold white]Value", style=COLORS.G.TEMPO), + show_footer=False, + show_header=False, + width=None, + pad_edge=False, + box=box.SIMPLE, + show_edge=True, + border_style="bright_black", + ) + + info_table.add_row("Crowdloan ID", f"#{crowdloan_id}") + info_table.add_row("Total Contributors", f"{crowdloan.contributors_count:,}") + info_table.add_row("Refund Limit (per call)", f"{refund_limit:,} contributors") + info_table.add_row("Amount to Refund", crowdloan.raised - crowdloan.deposit) + + if current_block >= crowdloan.end: + if crowdloan.raised < crowdloan.cap: + status = "[red]Failed[/red] (Cap not reached)" + else: + status = "[yellow]Ended but not finalized[/yellow]" + else: + status = "[green]Active[/green] (Still accepting contributions)" + + info_table.add_row("Status", status) + + refundable_contributors = max(0, crowdloan.contributors_count) + estimated_calls = ( + (refundable_contributors + refund_limit) // refund_limit + if refund_limit > 0 + else 0 + ) + + if estimated_calls > 1: + info_table.add_row( + "Estimated Calls Needed", + f"[yellow]~{estimated_calls}[/yellow] (due to contributor limit)", + ) + + console.print(info_table) From 73d5d24c82d59ebf77ea5869d39075e4ed9af13c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 20:10:08 -0700 Subject: [PATCH 38/69] refund_crowdloan --- bittensor_cli/src/commands/crowd/refund.py | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/bittensor_cli/src/commands/crowd/refund.py b/bittensor_cli/src/commands/crowd/refund.py index e44678a5..8134236c 100644 --- a/bittensor_cli/src/commands/crowd/refund.py +++ b/bittensor_cli/src/commands/crowd/refund.py @@ -121,3 +121,60 @@ async def refund_crowdloan( ) console.print(info_table) + + if estimated_calls > 1: + console.print( + f"\n[yellow]Note:[/yellow] Due to the [cyan]Refund Contributors Limit[/cyan] of {refund_limit:,} contributors per call,\n" + f" you may need to execute this command [yellow]{estimated_calls} times[/yellow] to refund all contributors.\n" + f" Each call will refund up to {refund_limit:,} contributors until all are processed.\n" + ) + + if prompt and not Confirm.ask( + f"\n[bold]Proceed with refunding contributors of Crowdloan #{crowdloan_id}?[/bold]", + default=False, + ): + console.print("[yellow]Refund cancelled.[/yellow]") + return False, "Refund cancelled by user." + + unlock_status = unlock_key(wallet) + if not unlock_status.success: + print_error(f"[red]{unlock_status.message}[/red]") + return False, unlock_status.message + + with console.status( + ":satellite: Submitting refund transaction...", spinner="aesthetic" + ): + call = await subtensor.substrate.compose_call( + call_module="Crowdloan", + call_function="refund", + call_params={ + "crowdloan_id": crowdloan_id, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + console.print("[green]Refund transaction submitted.[/green]") + return True, "Refund transaction submitted." + + await response.process_events() + + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed to refund contributors.[/red]\n" + f"{response.error_message}" + ) + return False, await response.error_message.get("name", "Unknown error") + console.print( + f"[green]Refund transaction succeeded![/green]\n" + f"Contributors have been refunded for Crowdloan #{crowdloan_id}." + ) + await print_extrinsic_id(response) + return True, "Refund completed successfully." From 5c6666787859832e58da37518efc5b8e1b1de6ff Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 20:16:57 -0700 Subject: [PATCH 39/69] update cli --- bittensor_cli/cli.py | 218 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 209 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 95ca015e..0b78c509 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -70,7 +70,10 @@ from bittensor_cli.src.commands.crowd import ( contribute as crowd_contribute, create as create_crowdloan, + dissolve as crowd_dissolve, view as view_crowdloan, + update as crowd_update, + refund as crowd_refund, ) from bittensor_cli.src.commands.liquidity.utils import ( prompt_liquidity, @@ -1159,6 +1162,24 @@ def __init__(self): self.crowd_app.command( "finalize", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] )(self.crowd_finalize) + self.crowd_app.command("list", rich_help_panel=HELP_PANELS["CROWD"]["INFO"])( + self.crowd_list + ) + self.crowd_app.command("info", rich_help_panel=HELP_PANELS["CROWD"]["INFO"])( + self.crowd_info + ) + self.crowd_app.command( + "create", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] + )(self.crowd_create) + self.crowd_app.command( + "update", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] + )(self.crowd_update) + self.crowd_app.command( + "refund", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] + )(self.crowd_refund) + self.crowd_app.command( + "dissolve", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] + )(self.crowd_dissolve) # Liquidity self.app.add_typer( @@ -1183,15 +1204,6 @@ def __init__(self): self.liquidity_app.command( "remove", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] )(self.liquidity_remove) - self.crowd_app.command("list", rich_help_panel=HELP_PANELS["CROWD"]["INFO"])( - self.crowd_list - ) - self.crowd_app.command("info", rich_help_panel=HELP_PANELS["CROWD"]["INFO"])( - self.crowd_info - ) - self.crowd_app.command( - "create", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] - )(self.crowd_create) # utils app self.utils_app.command("convert")(self.convert) @@ -7605,6 +7617,194 @@ def crowd_finalize( ) ) + def crowd_update( + self, + crowdloan_id: Optional[int] = typer.Option( + None, + "--crowdloan-id", + "--crowdloan_id", + "--id", + help="The ID of the crowdloan to update", + ), + min_contribution: Optional[float] = typer.Option( + None, + "--min-contribution", + "--min", + help="Update the minimum contribution amount (in TAO)", + ), + end: Optional[int] = typer.Option( + None, + "--end", + "--end-block", + help="Update the end block number", + ), + cap: Optional[float] = typer.Option( + None, + "--cap", + help="Update the cap amount (in TAO)", + ), + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Update one mutable field on a non-finalized crowdloan. + + Only the creator can invoke this. You may change the minimum contribution, + the end block, or the cap in a single call. When no flag is provided an + interactive prompt guides you through the update and validates the input + against the chain constants (absolute minimum contribution, block-duration + bounds, etc.). + """ + 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, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + min_contribution_balance = ( + Balance.from_tao(min_contribution) if min_contribution is not None else None + ) + cap_balance = Balance.from_tao(cap) if cap is not None else None + + return self._run_command( + crowd_update.update_crowdloan( + subtensor=self.initialize_chain(network), + wallet=wallet, + crowdloan_id=crowdloan_id, + min_contribution=min_contribution_balance, + end=end, + cap=cap_balance, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + ) + + def crowd_refund( + self, + crowdloan_id: Optional[int] = typer.Option( + None, + "--crowdloan-id", + "--crowdloan_id", + "--id", + help="The ID of the crowdloan to refund", + ), + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Refund contributors of a non-finalized crowdloan. + + Any account may call this once the crowdloan is no longer wanted. Each call + refunds up to the on-chain `RefundContributorsLimit` contributors (currently + 50) excluding the creator. Run it repeatedly until everyone except the creator + has been reimbursed. + """ + 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, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + return self._run_command( + crowd_refund.refund_crowdloan( + subtensor=self.initialize_chain(network), + wallet=wallet, + crowdloan_id=crowdloan_id, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + ) + + def crowd_dissolve( + self, + crowdloan_id: Optional[int] = typer.Option( + None, + "--crowdloan-id", + "--crowdloan_id", + "--id", + help="The ID of the crowdloan to dissolve", + ), + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Dissolve a crowdloan after all contributors have been refunded. + + Only the creator can dissolve. The crowdloan must be non-finalized and the + raised balance must equal the creator's own contribution (i.e., all other + contributions have been withdrawn or refunded). Dissolving returns the + creator's deposit and removes the crowdloan from storage. + + If there are funds still available other than the creator's contribution, + you can run `btcli crowd refund` to refund the remaining contributors. + """ + 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, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + return self._run_command( + crowd_dissolve.dissolve_crowdloan( + subtensor=self.initialize_chain(network), + wallet=wallet, + crowdloan_id=crowdloan_id, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + ) + @staticmethod def convert( from_rao: Optional[str] = typer.Option( From efa7c2b7f62d7f2f313a388940334e1e274e88fc Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 20:26:13 -0700 Subject: [PATCH 40/69] wip --- .../src/bittensor/subtensor_interface.py | 28 ++----------------- bittensor_cli/src/bittensor/utils.py | 22 --------------- 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 724e48b8..e589ac4b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -177,34 +177,12 @@ async def _decode_inline_call( """ Decode an `Option` returned from storage into a structured dictionary. """ - if not call_option: + if not call_option or "Inline" not in call_option: return None - - if isinstance(call_option, dict) and { - "call_module", - "call_function", - }.issubset(call_option.keys()): - return { - "call_index": call_option.get("call_index"), - "pallet": call_option.get("call_module"), - "method": call_option.get("call_function"), - "args": call_option.get("call_args", []), - "hash": call_option.get("call_hash"), - } - - inline_payload = None - if isinstance(call_option, dict): - inline_payload = call_option.get("Inline") or call_option.get("inline") - else: - inline_payload = call_option - - if inline_payload is None: - return None - - call_bytes = flatten_inline_call(inline_payload) + inline_bytes = bytes(call_option["Inline"][0][0]) call_obj = await self.substrate.create_scale_object( "Call", - data=ScaleBytes(call_bytes), + data=ScaleBytes(inline_bytes), block_hash=block_hash, ) call_value = call_obj.decode() diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 44647530..3846dc7b 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1426,28 +1426,6 @@ def hex_to_bytes(hex_str: str) -> bytes: return bytes_result -def flatten_inline_call(raw: Any) -> bytes: - """ - Convert SCALE inline call payloads to bytes. - """ - - def _to_bytes(value: Any) -> bytes: - if isinstance(value, (bytes, bytearray)): - return bytes(value) - if isinstance(value, int): - if not 0 <= value <= 255: - raise ValueError(f"Byte out of range: {value}") - return bytes((value,)) - if isinstance(value, str): - hex_str = value[2:] if value.startswith("0x") else value - return bytes.fromhex(hex_str) - if isinstance(value, (tuple, list)): - return b"".join(_to_bytes(item) for item in value) - raise TypeError(f"Unsupported call encoding: {type(value)!r}") - - return _to_bytes(raw) - - def blocks_to_duration(blocks: int) -> str: """Convert blocks to human readable duration string using two largest units. From 59950202d59a8d15d7b0ae2be66df4b2811956a6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 20:31:27 -0700 Subject: [PATCH 41/69] wip --- bittensor_cli/src/commands/crowd/create.py | 72 ++++++++++------------ 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 984fbd75..7e58b723 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -13,9 +13,8 @@ from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, - err_console, - is_valid_ss58_address, print_error, + is_valid_ss58_address, unlock_key, print_extrinsic_id, ) @@ -44,7 +43,7 @@ async def create_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: - err_console.print(f"[red]{unlock_status.message}[/red]") + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message crowdloan_type = None @@ -55,7 +54,7 @@ async def create_crowdloan( "\n[bold cyan]What type of crowdloan would you like to create?[/bold cyan]\n" "[cyan][1][/cyan] General Fundraising (funds go to address)\n" "[cyan][2][/cyan] Subnet Leasing (create new subnet)", - choices=["1", "2"] + choices=["1", "2"], ) crowdloan_type = "subnet" if type_choice == 2 else "fundraising" @@ -104,7 +103,7 @@ async def create_crowdloan( if duration_blocks is None: missing_fields.append("--duration") if missing_fields: - err_console.print( + print_error( "[red]The following options must be provided when prompts are disabled:[/red] " + ", ".join(missing_fields) ) @@ -120,12 +119,12 @@ async def create_crowdloan( deposit = Balance.from_tao(deposit_value) if deposit < minimum_deposit: if prompt: - err_console.print( + print_error( f"[red]Deposit must be at least {minimum_deposit.tao:,.4f} TAO.[/red]" ) deposit_value = None continue - err_console.print( + print_error( f"[red]Deposit is below the minimum required deposit " f"({minimum_deposit.tao:,.4f} TAO).[/red]" ) @@ -142,13 +141,13 @@ async def create_crowdloan( min_contribution = Balance.from_tao(min_contribution_value) if min_contribution < min_contribution: if prompt: - err_console.print( + print_error( f"[red]Minimum contribution must be at least " f"{min_contribution.tao:,.4f} TAO.[/red]" ) min_contribution_value = None continue - err_console.print( + print_error( "[red]Minimum contribution is below the chain's absolute minimum.[/red]" ) return False, "Minimum contribution is below the chain's absolute minimum." @@ -163,14 +162,12 @@ async def create_crowdloan( cap = Balance.from_tao(cap_value) if cap <= deposit: if prompt: - err_console.print( + print_error( f"[red]Cap must be greater than the deposit ({deposit.tao:,.4f} TAO).[/red]" ) cap_value = None continue - err_console.print( - "[red]Cap must be greater than the initial deposit.[/red]" - ) + print_error("[red]Cap must be greater than the initial deposit.[/red]") return False, "Cap must be greater than the initial deposit." break @@ -183,15 +180,13 @@ async def create_crowdloan( ) if duration_value < min_duration or duration_value > max_duration: if prompt: - err_console.print( + print_error( f"[red]Duration must be between {min_duration} and " f"{max_duration} blocks.[/red]" ) duration_value = None continue - err_console.print( - "[red]Crowdloan duration is outside the allowed range.[/red]" - ) + print_error("[red]Crowdloan duration is outside the allowed range.[/red]") return False, "Crowdloan duration is outside the allowed range." duration = duration_value break @@ -208,7 +203,7 @@ async def create_crowdloan( ) if not 0 <= emissions_share <= 100: - err_console.print( + print_error( f"[red]Emissions share must be between 0 and 100, got {emissions_share}[/red]" ) return False, "Invalid emissions share percentage." @@ -228,14 +223,14 @@ async def create_crowdloan( call_params={ "emissions_share": emissions_share, "end_block": None if lease_perpetual else lease_end_block, - } + }, ) call_to_attach = register_lease_call else: if target_address: target_address = target_address.strip() if not is_valid_ss58_address(target_address): - err_console.print( + print_error( f"[red]Invalid target SS58 address provided: {target_address}[/red]" ) return False, "Invalid target SS58 address provided." @@ -246,7 +241,7 @@ async def create_crowdloan( target_address = target_input.strip() or None if target_address and not is_valid_ss58_address(target_address): - err_console.print( + print_error( f"[red]Invalid target SS58 address provided: {target_address}[/red]" ) return False, "Invalid target SS58 address provided." @@ -255,7 +250,7 @@ async def create_crowdloan( creator_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) if deposit > creator_balance: - err_console.print( + print_error( f"[red]Insufficient balance to cover the deposit. " f"Available: {creator_balance}, required: {deposit}[/red]" ) @@ -297,7 +292,9 @@ async def create_crowdloan( if crowdloan_type == "subnet": table.add_row("Type", "[magenta]Subnet Leasing[/magenta]") - table.add_row("Emissions Share", f"[cyan]{emissions_share}%[/cyan] for contributors") + table.add_row( + "Emissions Share", f"[cyan]{emissions_share}%[/cyan] for contributors" + ) if lease_end_block: table.add_row("Lease Ends", f"Block {lease_end_block}") else: @@ -327,6 +324,7 @@ async def create_crowdloan( console.print("[yellow]Cancelled crowdloan creation.[/yellow]") return False, "Cancelled crowdloan creation." + # TODO: Update wait_fors + extrinsic_receipt after applying the patch success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -339,9 +337,7 @@ async def create_crowdloan( # extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() if not success: - err_console.print( - f"[red]{error_message or 'Failed to create crowdloan.'}[/red]" - ) + print_error(f"[red]{error_message or 'Failed to create crowdloan.'}[/red]") return False, error_message or "Failed to create crowdloan." if crowdloan_type == "subnet": @@ -412,22 +408,22 @@ async def finalize_crowdloan( ) if not crowdloan: - err_console.print(f"[red]Crowdloan #{crowdloan_id} does not exist.[/red]") + print_error(f"[red]Crowdloan #{crowdloan_id} does not exist.[/red]") return False, f"Crowdloan #{crowdloan_id} does not exist." if wallet.coldkeypub.ss58_address != crowdloan.creator: - err_console.print( + print_error( f"[red]Only the creator can finalize a crowdloan. " f"Creator: {crowdloan.creator}[/red]" ) return False, "Only the creator can finalize a crowdloan." if crowdloan.finalized: - err_console.print(f"[red]Crowdloan #{crowdloan_id} is already finalized.[/red]") + print_error(f"[red]Crowdloan #{crowdloan_id} is already finalized.[/red]") return False, "Crowdloan is already finalized." if crowdloan.raised < crowdloan.cap: - err_console.print( + print_error( f"[red]Crowdloan #{crowdloan_id} has not reached its cap.\n" f"Raised: {crowdloan.raised}, Cap: {crowdloan.cap}\n" f"Still needed: {Balance.from_rao(crowdloan.cap.rao - crowdloan.raised.rao)}[/red]" @@ -515,7 +511,7 @@ async def finalize_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: - err_console.print(f"[red]{unlock_status.message}[/red]") + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( @@ -526,7 +522,7 @@ async def finalize_crowdloan( ) if not success: - err_console.print( + print_error( f"[red]Failed to finalize: {error_message or 'Unknown error'}[/red]" ) return False, error_message or "Failed to finalize crowdloan." @@ -537,21 +533,17 @@ async def finalize_crowdloan( console.print( f"[bold]Finalization Complete:[/bold]\n" - f" • Total Raised: [{COLORS.S.AMOUNT}]{crowdloan.raised}[/{COLORS.S.AMOUNT}]\n" - f" • Contributors: {crowdloan.contributors_count}" + f"\t• Total Raised: [{COLORS.S.AMOUNT}]{crowdloan.raised}[/{COLORS.S.AMOUNT}]\n" + f"\t• Contributors: {crowdloan.contributors_count}" ) if crowdloan.target_address: console.print( - f" • Funds transferred to: [{COLORS.G.SUBHEAD_EX_1}]{crowdloan.target_address}[/{COLORS.G.SUBHEAD_EX_1}]" - ) - else: - console.print( - f" • Funds remain in: [{COLORS.G.SUBHEAD_EX_2}]{crowdloan.funds_account}[/{COLORS.G.SUBHEAD_EX_2}]" + f"\t• Funds transferred to: [{COLORS.G.SUBHEAD_EX_1}]{crowdloan.target_address}[/{COLORS.G.SUBHEAD_EX_1}]" ) if crowdloan.has_call: - console.print(" • [yellow]Associated call has been executed[/yellow]") + console.print("\t• [green]Associated call has been executed[/green]") if extrinsic_receipt: await print_extrinsic_id(extrinsic_receipt) From 0dabb4316b88b9bb3cc30abed9d95533879d69e1 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 20:31:36 -0700 Subject: [PATCH 42/69] almost --- .../src/commands/crowd/contribute.py | 26 +++++++++---------- bittensor_cli/src/commands/crowd/view.py | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index b0809f0b..532c380a 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -11,7 +11,7 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( console, - err_console, + print_error, print_extrinsic_id, unlock_key, ) @@ -76,14 +76,14 @@ async def contribute_to_crowdloan( subtensor.substrate.get_block_number(None), ) if not crowdloan: - err_console.print(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") + print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") return False, f"Crowdloan #{crowdloan_id} not found." is_valid, error_message = validate_for_contribution( crowdloan, crowdloan_id, current_block ) if not is_valid: - err_console.print(f"[red]{error_message}[/red]") + print_error(f"[red]{error_message}[/red]") return False, error_message contributor_address = wallet.coldkeypub.ss58_address @@ -116,13 +116,13 @@ async def contribute_to_crowdloan( contribution_amount = Balance.from_tao(amount) if contribution_amount < crowdloan.min_contribution: - err_console.print( + print_error( f"[red]Contribution amount ({contribution_amount}) is below minimum ({crowdloan.min_contribution}).[/red]" ) return False, "Contribution below minimum requirement." if contribution_amount > user_balance: - err_console.print( + print_error( f"[red]Insufficient balance. You have {user_balance} but trying to contribute {contribution_amount}.[/red]" ) return False, "Insufficient balance." @@ -206,7 +206,7 @@ async def contribute_to_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: - err_console.print(f"[red]{unlock_status.message}[/red]") + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message with console.status(f"\n:satellite: Contributing to crowdloan #{crowdloan_id}..."): @@ -222,7 +222,7 @@ async def contribute_to_crowdloan( ) if not success: - err_console.print(f"[red]Failed to contribute: {error_message}[/red]") + print_error(f"[red]Failed to contribute: {error_message}[/red]") return False, error_message or "Failed to contribute." new_balance, new_contribution, updated_crowdloan = await asyncio.gather( @@ -300,11 +300,11 @@ async def withdraw_from_crowdloan( ) if not crowdloan: - err_console.print(f"[red]Crowdloan #{crowdloan_id} does not exist.[/red]") + print_error(f"[red]Crowdloan #{crowdloan_id} does not exist.[/red]") return False, f"Crowdloan #{crowdloan_id} does not exist." if crowdloan.finalized: - err_console.print( + print_error( f"[red]Crowdloan #{crowdloan_id} is already finalized. Withdrawals are not allowed.[/red]" ) return False, "Cannot withdraw from finalized crowdloan." @@ -317,7 +317,7 @@ async def withdraw_from_crowdloan( ) if user_contribution == Balance.from_tao(0): - err_console.print( + print_error( f"[red]You have no contribution to withdraw from crowdloan #{crowdloan_id}.[/red]" ) return False, "No contribution to withdraw." @@ -326,7 +326,7 @@ async def withdraw_from_crowdloan( if is_creator: withdrawable = user_contribution - crowdloan.deposit if withdrawable <= 0: - err_console.print( + print_error( f"[red]As the creator, you cannot withdraw your deposit of {crowdloan.deposit}. " f"Only contributions above the deposit can be withdrawn.[/red]" ) @@ -409,7 +409,7 @@ async def withdraw_from_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: - err_console.print(f"[red]{unlock_status.message}[/red]") + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message with console.status(f"\n:satellite: Withdrawing from crowdloan #{crowdloan_id}..."): @@ -425,7 +425,7 @@ async def withdraw_from_crowdloan( ) if not success: - err_console.print( + print_error( f"[red]Failed to withdraw: {error_message or 'Unknown error'}[/red]" ) return False, error_message or "Failed to withdraw from crowdloan." diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index 50c0967a..36d9950b 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -12,7 +12,7 @@ from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, - err_console, + print_error, millify_tao, ) @@ -252,7 +252,7 @@ async def show_crowdloan_details( subtensor.get_single_crowdloan(crowdloan_id), ) if not crowdloan: - err_console.print(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") + print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") return False, f"Crowdloan #{crowdloan_id} not found." user_contribution = None From 6380fb01f6f5dec21230ac4e1f62f88f1d400434 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 21:06:26 -0700 Subject: [PATCH 43/69] update --- bittensor_cli/src/commands/crowd/create.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 7e58b723..0aab1fd8 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -236,11 +236,11 @@ async def create_crowdloan( return False, "Invalid target SS58 address provided." elif prompt: target_input = Prompt.ask( - "Enter a target SS58 address (leave blank for none)", + "Enter a target SS58 address", ) target_address = target_input.strip() or None - if target_address and not is_valid_ss58_address(target_address): + if not is_valid_ss58_address(target_address): print_error( f"[red]Invalid target SS58 address provided: {target_address}[/red]" ) @@ -475,11 +475,6 @@ async def finalize_crowdloan( "Funds Will Go To", f"[{COLORS.G.SUBHEAD_EX_1}]{crowdloan.target_address}[/{COLORS.G.SUBHEAD_EX_1}]", ) - else: - table.add_row( - "Funds Will Go To", - "[yellow]Funds Account (manual transfer required)[/yellow]", - ) if crowdloan.has_call: table.add_row( From 6092821d848a5cef6fd91c552896dbba4c80ffd2 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 15 Oct 2025 21:06:39 -0700 Subject: [PATCH 44/69] update docs --- bittensor_cli/cli.py | 28 +++++++++++++++++-- .../src/bittensor/subtensor_interface.py | 1 - 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0b78c509..9b8c2df0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7284,7 +7284,21 @@ def crowd_list( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """List all crowdloans.""" + """ + List crowdloans together with their funding progress and key metadata. + + Shows every crowdloan on the selected network, including current status + (Active, Funded, Closed, Finalized), whether it is a subnet leasing crowdloan, + or a general fundraising crowdloan. + + Use `--verbose` for full-precision amounts and longer addresses. + + EXAMPLES + + [green]$[/green] btcli crowd list + + [green]$[/green] btcli crowd list --verbose + """ self.verbosity_handler(quiet, verbose, False) return self._run_command( view_crowdloan.list_crowdloans( @@ -7310,7 +7324,17 @@ def crowd_info( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Display detailed information about a specific crowdloan.""" + """ + Display detailed information about a specific crowdloan. + + Includes funding progress, target account, and call details among other information. + + EXAMPLES + + [green]$[/green] btcli crowd info --id 0 + + [green]$[/green] btcli crowd info --id 1 --verbose + """ self.verbosity_handler(quiet, verbose, False) if crowdloan_id is None: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e589ac4b..99a524c6 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -44,7 +44,6 @@ u16_normalized_float, U16_MAX, get_hotkey_pub_ss58, - flatten_inline_call, ) From ef8f2dc76af97a48258b149c3bfd722d6f77dd2f Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 16 Oct 2025 16:12:53 +0200 Subject: [PATCH 45/69] Fixes decoding of extrinsic --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d7e6150f..31df9fce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0.1", "rich>=13.7,<15.0", - "scalecodec==1.2.11", + "scalecodec==1.2.12", "typer>=0.16", "bittensor-wallet>=4.0.0", "packaging", From 1724413af9c4aa0d4c10ef0525532ca5762a03b5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 16 Oct 2025 17:04:52 +0200 Subject: [PATCH 46/69] Skips senate tests --- tests/e2e_tests/test_senate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index c4cbebd7..868f3902 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -9,10 +9,14 @@ import asyncio import json +import pytest from .utils import call_add_proposal +@pytest.mark.skip( + reason="See: https://github.com/opentensor/bittensor/pull/3102. Skipping until new governance is set up." +) def test_senate(local_chain, wallet_setup): """ Test the senate functionality in Bittensor From 5067bbb86bbbc53f53014e9d204c5cb7b22a2bd7 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 16 Oct 2025 18:21:41 +0200 Subject: [PATCH 47/69] Removes the fetching of identities in GitHub. --- .../src/bittensor/subtensor_interface.py | 64 +++++-------------- 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index fe5e7af2..21775af5 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -3,7 +3,6 @@ import time from typing import Optional, Any, Union, TypedDict, Iterable -import aiohttp from async_substrate_interface import AsyncExtrinsicReceipt from async_substrate_interface.async_substrate import ( DiskCachedAsyncSubstrateInterface, @@ -1301,56 +1300,23 @@ async def get_delegate_identities( :return: {ss58: DelegatesDetails, ...} """ - timeout = aiohttp.ClientTimeout(10.0) - async with aiohttp.ClientSession(timeout=timeout) as session: - identities_info, response = await asyncio.gather( - self.substrate.query_map( - module="Registry", - storage_function="IdentityOf", - block_hash=block_hash, - ), - session.get(Constants.delegates_detail_url), - ) - - all_delegates_details = {} - async for ss58_address, identity in identities_info: - all_delegates_details.update( - { - decode_account_id( - ss58_address[0] - ): DelegatesDetails.from_chain_data( - decode_hex_identity_dict(identity.value["info"]) - ) - } - ) + identities_info = await self.substrate.query_map( + module="Registry", + storage_function="IdentityOf", + block_hash=block_hash, + ) - if response.ok: - all_delegates: dict[str, Any] = await response.json(content_type=None) - - for delegate_hotkey, delegate_details in all_delegates.items(): - delegate_info = all_delegates_details.setdefault( - delegate_hotkey, - DelegatesDetails( - display=delegate_details.get("name", ""), - web=delegate_details.get("url", ""), - additional=delegate_details.get("description", ""), - pgp_fingerprint=delegate_details.get("fingerprint", ""), - ), - ) - delegate_info.display = ( - delegate_info.display or delegate_details.get("name", "") - ) - delegate_info.web = delegate_info.web or delegate_details.get( - "url", "" - ) - delegate_info.additional = ( - delegate_info.additional - or delegate_details.get("description", "") - ) - delegate_info.pgp_fingerprint = ( - delegate_info.pgp_fingerprint - or delegate_details.get("fingerprint", "") + all_delegates_details = {} + async for ss58_address, identity in identities_info: + all_delegates_details.update( + { + decode_account_id( + ss58_address[0] + ): DelegatesDetails.from_chain_data( + decode_hex_identity_dict(identity.value["info"]) ) + } + ) return all_delegates_details From 2a98b2a44acfd2d9e269d06c01b9c5d460bd544e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 10:07:37 -0700 Subject: [PATCH 48/69] add back finalization after scalecodec update --- bittensor_cli/src/commands/crowd/create.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 0aab1fd8..07115c89 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -324,17 +324,16 @@ async def create_crowdloan( console.print("[yellow]Cancelled crowdloan creation.[/yellow]") return False, "Cancelled crowdloan creation." - # TODO: Update wait_fors + extrinsic_receipt after applying the patch success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, - wait_for_inclusion=False, - wait_for_finalization=False, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) extrinsic_id = None - # if extrinsic_receipt: - # extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + if extrinsic_receipt: + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() if not success: print_error(f"[red]{error_message or 'Failed to create crowdloan.'}[/red]") From 23ea5faf08611992e210a6c4363e28b3137d125c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 13:56:13 -0700 Subject: [PATCH 49/69] update dissolve --- bittensor_cli/src/commands/crowd/dissolve.py | 29 ++++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/dissolve.py b/bittensor_cli/src/commands/crowd/dissolve.py index 8a09da49..ccb766b0 100644 --- a/bittensor_cli/src/commands/crowd/dissolve.py +++ b/bittensor_cli/src/commands/crowd/dissolve.py @@ -13,7 +13,6 @@ print_extrinsic_id, print_error, unlock_key, - format_error_message, ) @@ -131,29 +130,23 @@ async def dissolve_crowdloan( call_function="dissolve", call_params={"crowdloan_id": crowdloan_id}, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( + ( + success, + error_message, + extrinsic_receipt, + ) = await subtensor.sign_and_send_extrinsic( call=call, - keypair=wallet.coldkey, - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - if not wait_for_finalization and not wait_for_inclusion: - console.print("[green]Dissolve transaction submitted.[/green]") - return True, "Dissolve transaction submitted." - - await response.process_events() - - if not await response.is_success: - print_error( - f"[red]Failed to dissolve crowdloan.[/red]\n{format_error_message(await response.error_message)}" - ) - return False, format_error_message(await response.error_message) + if not success: + print_error(f"[red]Failed to dissolve crowdloan.[/red]\n{error_message}") + return False, error_message - await print_extrinsic_id(response) + if extrinsic_receipt: + await print_extrinsic_id(extrinsic_receipt) console.print("[green]Crowdloan dissolved successfully![/green]") From 902ff641f799b3a7e0c0a5fa79e90718145e4805 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 13:56:33 -0700 Subject: [PATCH 50/69] update interface --- bittensor_cli/src/bittensor/subtensor_interface.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 99a524c6..8daf9487 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1782,7 +1782,7 @@ async def get_single_crowdloan( The returned data includes crowdloan details such as funding targets, contribution minimums, timeline, and current funding status """ - crowdloan_info = await self.substrate.query( + crowdloan_info = await self.query( module="Crowdloan", storage_function="Crowdloans", params=[crowdloan_id], @@ -1816,15 +1816,15 @@ async def get_crowdloan_contribution( This function queries the Contributions storage to find the amount a specific address has contributed to a given crowdloan. """ - contribution = await self.substrate.query( + contribution = await self.query( module="Crowdloan", storage_function="Contributions", params=[crowdloan_id, contributor], block_hash=block_hash, ) - if contribution and contribution.value: - return Balance.from_rao(contribution.value) + if contribution: + return Balance.from_rao(contribution) return None async def get_coldkey_swap_schedule_duration( From 465c20bd3203487921825f3bd2e8a37cb51d86af Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 13:56:46 -0700 Subject: [PATCH 51/69] update refund --- bittensor_cli/src/commands/crowd/refund.py | 35 ++++++++++------------ 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/refund.py b/bittensor_cli/src/commands/crowd/refund.py index 8134236c..dc42f388 100644 --- a/bittensor_cli/src/commands/crowd/refund.py +++ b/bittensor_cli/src/commands/crowd/refund.py @@ -151,30 +151,25 @@ async def refund_crowdloan( "crowdloan_id": crowdloan_id, }, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, + ( + success, + error_message, + extrinsic_receipt, + ) = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - if not wait_for_finalization and not wait_for_inclusion: - console.print("[green]Refund transaction submitted.[/green]") - return True, "Refund transaction submitted." - - await response.process_events() + if not success: + print_error(f"[red]Failed to refund contributors.[/red]\n{error_message}") + return False, error_message - if not await response.is_success: - print_error( - f":cross_mark: [red]Failed to refund contributors.[/red]\n" - f"{response.error_message}" - ) - return False, await response.error_message.get("name", "Unknown error") console.print( - f"[green]Refund transaction succeeded![/green]\n" - f"Contributors have been refunded for Crowdloan #{crowdloan_id}." + f"[green]Contributors have been refunded for Crowdloan #{crowdloan_id}.[/green]" ) - await print_extrinsic_id(response) - return True, "Refund completed successfully." + if extrinsic_receipt: + await print_extrinsic_id(extrinsic_receipt) + + return True, "Contributors have been refunded for Crowdloan #{crowdloan_id}." From ffceec49442a5b8b11ee9ebbb47b72d3982fe807 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 13:57:11 -0700 Subject: [PATCH 52/69] loan updates --- bittensor_cli/src/commands/crowd/update.py | 43 +++++++++------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py index 93c5aa0e..416e892e 100644 --- a/bittensor_cli/src/commands/crowd/update.py +++ b/bittensor_cli/src/commands/crowd/update.py @@ -299,45 +299,36 @@ async def update_crowdloan( if call_function == "update_min_contribution": value = value.rao - with console.status(f":satellite: Updating {update_type}...", spinner="earth"): + with console.status( + ":satellite: Submitting update transaction...", spinner="aesthetic" + ): call = await subtensor.substrate.compose_call( call_module="Crowdloan", call_function=call_function, call_params={"crowdloan_id": crowdloan_id, param_name: value}, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - - with console.status( - ":satellite: Submitting update transaction...", spinner="aesthetic" - ): - response = await subtensor.substrate.submit_extrinsic( - extrinsic, + ( + success, + error_message, + extrinsic_receipt, + ) = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - if not wait_for_finalization and not wait_for_inclusion: - console.print( - ":white_heavy_check_mark: [green]Update transaction submitted.[/green]" - ) - return True, "Update transaction submitted." - - await response.process_events() - - if not await response.is_success: - print_error( - f":cross_mark: [red]Failed to update {update_type}.[/red]\n" - f"{response.error_message}" - ) - return False, response.error_message.get("name", "Unknown error") + if not success: + print_error(f"[red]Failed to update {update_type}.[/red]\n{error_message}") + return False, error_message console.print( - f":white_heavy_check_mark: [green]{update_type} updated successfully![/green]\n" + f"[green]{update_type} updated successfully![/green]\n" f"Crowdloan #{crowdloan_id} has been updated." ) - await print_extrinsic_id(response) + if extrinsic_receipt: + await print_extrinsic_id(extrinsic_receipt) + return True, f"{update_type} updated successfully." From 341b0a3856f0a90c8733c2531c2b9ea130a68aa0 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 14:13:23 -0700 Subject: [PATCH 53/69] update get_constant for crowdloans --- bittensor_cli/src/commands/crowd/create.py | 10 ++++---- bittensor_cli/src/commands/crowd/update.py | 24 +++++++++---------- bittensor_cli/src/commands/crowd/utils.py | 28 +++++++++++++++++++++- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 07115c89..561c03fc 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -77,16 +77,18 @@ async def create_crowdloan( print_error("Crowdloan type not specified and no prompt provided.") return False, "Crowdloan type not specified and no prompt provided." + block_hash = await subtensor.substrate.get_chain_head() + runtime = await subtensor.substrate.init_runtime(block_hash=block_hash) ( minimum_deposit_raw, min_contribution_raw, min_duration, max_duration, ) = await asyncio.gather( - get_constant(subtensor, "MinimumDeposit"), - get_constant(subtensor, "AbsoluteMinimumContribution"), - get_constant(subtensor, "MinimumBlockDuration"), - get_constant(subtensor, "MaximumBlockDuration"), + get_constant(subtensor, "MinimumDeposit", runtime=runtime), + get_constant(subtensor, "AbsoluteMinimumContribution", runtime=runtime), + get_constant(subtensor, "MinimumBlockDuration", runtime=runtime), + get_constant(subtensor, "MaximumBlockDuration", runtime=runtime), ) minimum_deposit = Balance.from_rao(minimum_deposit_raw) diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py index 416e892e..2c6e757f 100644 --- a/bittensor_cli/src/commands/crowd/update.py +++ b/bittensor_cli/src/commands/crowd/update.py @@ -47,18 +47,17 @@ async def update_crowdloan( tuple[bool, str]: Success status and message """ - ( - crowdloan, - current_block, - absolute_min_rao, - min_duration, - max_duration, - ) = await asyncio.gather( - subtensor.get_single_crowdloan(crowdloan_id), - subtensor.substrate.get_block_number(None), - get_constant(subtensor, "AbsoluteMinimumContribution"), - get_constant(subtensor, "MinimumBlockDuration"), - get_constant(subtensor, "MaximumBlockDuration"), + block_hash = await subtensor.substrate.get_chain_head() + crowdloan, current_block = await asyncio.gather( + subtensor.get_single_crowdloan(crowdloan_id, block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), + ) + + runtime = await subtensor.substrate.init_runtime(block_hash=block_hash) + absolute_min_rao, min_duration, max_duration = await asyncio.gather( + get_constant(subtensor, "AbsoluteMinimumContribution", runtime=runtime), + get_constant(subtensor, "MinimumBlockDuration", runtime=runtime), + get_constant(subtensor, "MaximumBlockDuration", runtime=runtime), ) absolute_min = Balance.from_rao(absolute_min_rao) @@ -330,5 +329,4 @@ async def update_crowdloan( if extrinsic_receipt: await print_extrinsic_id(extrinsic_receipt) - return True, f"{update_type} updated successfully." diff --git a/bittensor_cli/src/commands/crowd/utils.py b/bittensor_cli/src/commands/crowd/utils.py index c405d882..4ad7895e 100644 --- a/bittensor_cli/src/commands/crowd/utils.py +++ b/bittensor_cli/src/commands/crowd/utils.py @@ -1,9 +1,35 @@ +from typing import Optional + +from async_substrate_interface.types import Runtime + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -async def get_constant(subtensor: SubtensorInterface, constant_name: str) -> int: +async def get_constant( + subtensor: SubtensorInterface, + constant_name: str, + runtime: Optional[Runtime] = None, + block_hash: Optional[str] = None, +) -> int: + """ + Get a constant from the Crowdloan pallet. + + Args: + subtensor: SubtensorInterface object for chain interaction + constant_name: Name of the constant to get + runtime: Runtime object + block_hash: Block hash + + Returns: + The value of the constant + """ + + runtime = runtime or await subtensor.substrate.init_runtime(block_hash=block_hash) + result = await subtensor.substrate.get_constant( module_name="Crowdloan", constant_name=constant_name, + block_hash=block_hash, + runtime=runtime, ) return getattr(result, "value", result) From a91ab680e03db36256ef4f9d407dd0bcee03661f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 15:22:30 -0700 Subject: [PATCH 54/69] add json outputs to contribute --- bittensor_cli/cli.py | 8 +- .../src/commands/crowd/contribute.py | 270 +++++++++++++----- 2 files changed, 212 insertions(+), 66 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9b8c2df0..e43a0f75 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7498,6 +7498,7 @@ def crowd_contribute( wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """Contribute TAO to an active crowdloan. @@ -7510,7 +7511,7 @@ def crowd_contribute( [green]$[/green] btcli crowd contribute --id 1 """ - self.verbosity_handler(quiet, verbose, False) + self.verbosity_handler(quiet, verbose, json_output) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -7536,6 +7537,7 @@ def crowd_contribute( prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + json_output=json_output, ) ) @@ -7557,6 +7559,7 @@ def crowd_withdraw( wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Withdraw contributions from a non-finalized crowdloan. @@ -7564,6 +7567,8 @@ def crowd_withdraw( Non-creators can withdraw their full contribution. Creators can only withdraw amounts above their initial deposit. """ + self.verbosity_handler(quiet, verbose, json_output) + if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -7587,6 +7592,7 @@ def crowd_withdraw( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, + json_output=json_output, ) ) diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index 532c380a..9ea8b19a 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -1,3 +1,4 @@ +import json from typing import Optional from async_substrate_interface.utils.cache import asyncio @@ -11,6 +12,7 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( console, + json_console, print_error, print_extrinsic_id, unlock_key, @@ -56,6 +58,7 @@ async def contribute_to_crowdloan( prompt: bool, wait_for_inclusion: bool, wait_for_finalization: bool, + json_output: bool = False, ) -> tuple[bool, str]: """Contribute TAO to an active crowdloan. @@ -76,14 +79,21 @@ async def contribute_to_crowdloan( subtensor.substrate.get_block_number(None), ) if not crowdloan: - print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") - return False, f"Crowdloan #{crowdloan_id} not found." + error_msg = f"Crowdloan #{crowdloan_id} not found." + 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 is_valid, error_message = validate_for_contribution( crowdloan, crowdloan_id, current_block ) if not is_valid: - print_error(f"[red]{error_message}[/red]") + if json_output: + json_console.print(json.dumps({"success": False, "error": error_message})) + else: + print_error(f"[red]{error_message}[/red]") return False, error_message contributor_address = wallet.coldkeypub.ss58_address @@ -116,15 +126,19 @@ async def contribute_to_crowdloan( contribution_amount = Balance.from_tao(amount) if contribution_amount < crowdloan.min_contribution: - print_error( - f"[red]Contribution amount ({contribution_amount}) is below minimum ({crowdloan.min_contribution}).[/red]" - ) + error_msg = f"Contribution amount ({contribution_amount}) is below minimum ({crowdloan.min_contribution})." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, "Contribution below minimum requirement." if contribution_amount > user_balance: - print_error( - f"[red]Insufficient balance. You have {user_balance} but trying to contribute {contribution_amount}.[/red]" - ) + error_msg = f"Insufficient balance. You have {user_balance} but trying to contribute {contribution_amount}." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, "Insufficient balance." # Auto-adjustment @@ -201,12 +215,24 @@ async def contribute_to_crowdloan( if prompt: if not Confirm.ask("\nProceed with contribution?"): - console.print("[yellow]Contribution cancelled.[/yellow]") + if json_output: + json_console.print( + json.dumps( + {"success": False, "error": "Contribution cancelled by user."} + ) + ) + else: + console.print("[yellow]Contribution cancelled.[/yellow]") return False, "Contribution cancelled by user." unlock_status = unlock_key(wallet) if not unlock_status.success: - print_error(f"[red]{unlock_status.message}[/red]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": unlock_status.message}) + ) + else: + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message with console.status(f"\n:satellite: Contributing to crowdloan #{crowdloan_id}..."): @@ -222,7 +248,17 @@ async def contribute_to_crowdloan( ) if not success: - print_error(f"[red]Failed to contribute: {error_message}[/red]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": error_message or "Failed to contribute.", + } + ) + ) + else: + print_error(f"[red]Failed to contribute: {error_message}[/red]") return False, error_message or "Failed to contribute." new_balance, new_contribution, updated_crowdloan = await asyncio.gather( @@ -231,38 +267,76 @@ async def contribute_to_crowdloan( subtensor.get_single_crowdloan(crowdloan_id), ) - console.print( - f"\n[dark_sea_green3]Successfully contributed to crowdloan #{crowdloan_id}![/dark_sea_green3]" - ) - - console.print( - f"Balance:\n [blue]{user_balance}[/blue] → " - f"[{COLORS.S.AMOUNT}]{new_balance}[/{COLORS.S.AMOUNT}]" - ) - - if new_contribution: - if current_contribution: - console.print( - f"Your Contribution:\n [blue]{current_contribution}[/blue] → " - f"[{COLORS.S.AMOUNT}]{new_contribution}[/{COLORS.S.AMOUNT}]" - ) - else: - console.print( - f"Your Contribution: [{COLORS.S.AMOUNT}]{new_contribution}[/{COLORS.S.AMOUNT}]" - ) + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + if json_output: + output_dict = { + "success": True, + "error": None, + "extrinsic_identifier": extrinsic_id, + "data": { + "crowdloan_id": crowdloan_id, + "contributor": contributor_address, + "contribution_amount": actual_contribution.tao, + "previous_contribution": current_contribution.tao + if current_contribution + else 0.0, + "total_contribution": new_contribution.tao if new_contribution else 0.0, + "balance": { + "before": user_balance.tao, + "after": new_balance.tao, + "fee": extrinsic_fee.tao, + }, + "crowdloan": { + "raised_before": crowdloan.raised.tao, + "raised_after": updated_crowdloan.raised.tao + if updated_crowdloan + else crowdloan.raised.tao, + "cap": crowdloan.cap.tao, + "percentage": ( + updated_crowdloan.raised.tao / updated_crowdloan.cap.tao * 100 + ) + if updated_crowdloan + else 0.0, + }, + "adjusted": will_be_adjusted, + "cap_reached": updated_crowdloan.raised >= updated_crowdloan.cap + if updated_crowdloan + else False, + }, + } + json_console.print(json.dumps(output_dict)) + else: + console.print( + f"\n[dark_sea_green3]Successfully contributed to crowdloan #{crowdloan_id}![/dark_sea_green3]" + ) - if updated_crowdloan: console.print( - f"Crowdloan Progress:\n [blue]{crowdloan.raised}[/blue] → " - f"[{COLORS.S.AMOUNT}]{updated_crowdloan.raised}[/{COLORS.S.AMOUNT}] / {updated_crowdloan.cap}" + f"Balance:\n [blue]{user_balance}[/blue] → " + f"[{COLORS.S.AMOUNT}]{new_balance}[/{COLORS.S.AMOUNT}]" ) - if updated_crowdloan.raised >= updated_crowdloan.cap: + if new_contribution: + if current_contribution: + console.print( + f"Your Contribution:\n [blue]{current_contribution}[/blue] → " + f"[{COLORS.S.AMOUNT}]{new_contribution}[/{COLORS.S.AMOUNT}]" + ) + else: + console.print( + f"Your Contribution: [{COLORS.S.AMOUNT}]{new_contribution}[/{COLORS.S.AMOUNT}]" + ) + + if updated_crowdloan: console.print( - "\n[bold green]🎉 Crowdloan has reached its funding cap![/bold green]" + f"Crowdloan Progress:\n [blue]{crowdloan.raised}[/blue] → " + f"[{COLORS.S.AMOUNT}]{updated_crowdloan.raised}[/{COLORS.S.AMOUNT}] / {updated_crowdloan.cap}" ) - if extrinsic_receipt: + if updated_crowdloan.raised >= updated_crowdloan.cap: + console.print( + "\n[bold green]🎉 Crowdloan has reached its funding cap![/bold green]" + ) + await print_extrinsic_id(extrinsic_receipt) return True, "Successfully contributed to crowdloan." @@ -275,6 +349,7 @@ async def withdraw_from_crowdloan( wait_for_inclusion: bool, wait_for_finalization: bool, prompt: bool, + json_output: bool = False, ) -> tuple[bool, str]: """ Withdraw contributions from a non-finalized crowdloan. @@ -300,13 +375,19 @@ async def withdraw_from_crowdloan( ) if not crowdloan: - print_error(f"[red]Crowdloan #{crowdloan_id} does not exist.[/red]") - return False, f"Crowdloan #{crowdloan_id} does not exist." + error_msg = f"Crowdloan #{crowdloan_id} does not exist." + 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.finalized: - print_error( - f"[red]Crowdloan #{crowdloan_id} is already finalized. Withdrawals are not allowed.[/red]" - ) + error_msg = f"Crowdloan #{crowdloan_id} is already finalized. Withdrawals are not allowed." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, "Cannot withdraw from finalized crowdloan." user_contribution, user_balance = await asyncio.gather( @@ -317,19 +398,24 @@ async def withdraw_from_crowdloan( ) if user_contribution == Balance.from_tao(0): - print_error( - f"[red]You have no contribution to withdraw from crowdloan #{crowdloan_id}.[/red]" + error_msg = ( + f"You have no contribution to withdraw from crowdloan #{crowdloan_id}." ) + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, "No contribution to withdraw." is_creator = wallet.coldkeypub.ss58_address == crowdloan.creator if is_creator: withdrawable = user_contribution - crowdloan.deposit if withdrawable <= 0: - print_error( - f"[red]As the creator, you cannot withdraw your deposit of {crowdloan.deposit}. " - f"Only contributions above the deposit can be withdrawn.[/red]" - ) + error_msg = f"As the creator, you cannot withdraw your deposit of {crowdloan.deposit}. Only contributions above the deposit can be withdrawn." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, "Creator cannot withdraw deposit amount." remaining_contribution = crowdloan.deposit else: @@ -404,12 +490,24 @@ async def withdraw_from_crowdloan( console.print(table) if not Confirm.ask("\nProceed with withdrawal?"): - console.print("[yellow]Withdrawal cancelled.[/yellow]") + if json_output: + json_console.print( + json.dumps( + {"success": False, "error": "Withdrawal cancelled by user."} + ) + ) + else: + console.print("[yellow]Withdrawal cancelled.[/yellow]") return False, "Withdrawal cancelled by user." unlock_status = unlock_key(wallet) if not unlock_status.success: - print_error(f"[red]{unlock_status.message}[/red]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": unlock_status.message}) + ) + else: + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message with console.status(f"\n:satellite: Withdrawing from crowdloan #{crowdloan_id}..."): @@ -425,33 +523,75 @@ async def withdraw_from_crowdloan( ) if not success: - print_error( - f"[red]Failed to withdraw: {error_message or 'Unknown error'}[/red]" - ) + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": error_message or "Failed to withdraw from crowdloan.", + } + ) + ) + else: + print_error( + f"[red]Failed to withdraw: {error_message or 'Unknown error'}[/red]" + ) return False, error_message or "Failed to withdraw from crowdloan." - console.print( - f"\n✅ [green]Successfully withdrew from crowdloan #{crowdloan_id}![/green]\n" - ) - - new_balance, updated_contribution = await asyncio.gather( + new_balance, updated_contribution, updated_crowdloan = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), subtensor.get_crowdloan_contribution( crowdloan_id, wallet.coldkeypub.ss58_address ), + subtensor.get_single_crowdloan(crowdloan_id), ) - console.print( - f"Amount Withdrawn: [{COLORS.S.AMOUNT}]{withdrawable}[/{COLORS.S.AMOUNT}]\n" - f"Balance:\n [blue]{user_balance}[/blue] → [{COLORS.S.AMOUNT}]{new_balance}[/{COLORS.S.AMOUNT}]" - ) + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + if json_output: + output_dict = { + "success": True, + "error": None, + "extrinsic_identifier": extrinsic_id, + "data": { + "crowdloan_id": crowdloan_id, + "is_creator": is_creator, + "withdrawal_amount": withdrawable.tao, + "previous_contribution": user_contribution.tao, + "remaining_contribution": updated_contribution.tao + if updated_contribution + else 0.0, + "deposit_locked": crowdloan.deposit.tao if is_creator else None, + "balance": { + "before": user_balance.tao, + "after": new_balance.tao, + "fee": extrinsic_fee.tao, + }, + "crowdloan": { + "raised_before": crowdloan.raised.tao, + "raised_after": updated_crowdloan.raised.tao + if updated_crowdloan + else (crowdloan.raised.tao - withdrawable.tao), + }, + }, + } + json_console.print(json.dumps(output_dict)) + else: + console.print( + f"\n✅ [green]Successfully withdrew from crowdloan #{crowdloan_id}![/green]\n" + ) - if is_creator and updated_contribution: console.print( - f"Remaining Contribution: [{COLORS.S.AMOUNT}]{updated_contribution}[/{COLORS.S.AMOUNT}] (deposit locked)" + f"Amount Withdrawn: [{COLORS.S.AMOUNT}]{withdrawable}[/{COLORS.S.AMOUNT}]\n" + f"Balance:\n [blue]{user_balance}[/blue] → [{COLORS.S.AMOUNT}]{new_balance}[/{COLORS.S.AMOUNT}]" + f"Crowdloan raised before: [{COLORS.S.AMOUNT}]{crowdloan.raised}[/{COLORS.S.AMOUNT}]" + f"Crowdloan raised after: [{COLORS.S.AMOUNT}]{updated_crowdloan.raised}[/{COLORS.S.AMOUNT}]" ) - if extrinsic_receipt: + if is_creator and updated_contribution: + console.print( + f"Remaining Contribution: [{COLORS.S.AMOUNT}]{updated_contribution}[/{COLORS.S.AMOUNT}] (deposit locked)" + ) + await print_extrinsic_id(extrinsic_receipt) return True, "Successfully withdrew from crowdloan." From d08c31ebf6f7804300cadf6d57587458f75793ab Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 15:36:52 -0700 Subject: [PATCH 55/69] json in finalize call --- bittensor_cli/cli.py | 4 + bittensor_cli/src/commands/crowd/create.py | 118 ++++++++++++++++----- 2 files changed, 93 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e43a0f75..53024ea8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7614,6 +7614,7 @@ def crowd_finalize( wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Finalize a successful crowdloan that has reached its cap. @@ -7621,6 +7622,8 @@ def crowd_finalize( Only the creator can finalize. This will transfer funds to the target address (if specified) and execute any attached call (e.g., subnet creation). """ + self.verbosity_handler(quiet, verbose, json_output) + if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -7644,6 +7647,7 @@ def crowd_finalize( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, + json_output=json_output, ) ) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 561c03fc..84eeffeb 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -1,4 +1,5 @@ import asyncio +import json from typing import Optional from bittensor_wallet import Wallet @@ -13,6 +14,7 @@ from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, + json_console, print_error, is_valid_ss58_address, unlock_key, @@ -382,6 +384,7 @@ async def finalize_crowdloan( wait_for_inclusion: bool, wait_for_finalization: bool, prompt: bool, + json_output: bool = False, ) -> tuple[bool, str]: """ Finalize a successful crowdloan that has reached its cap. @@ -409,26 +412,45 @@ async def finalize_crowdloan( ) if not crowdloan: - print_error(f"[red]Crowdloan #{crowdloan_id} does not exist.[/red]") - return False, f"Crowdloan #{crowdloan_id} does not exist." + error_msg = f"Crowdloan #{crowdloan_id} does not exist." + 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 wallet.coldkeypub.ss58_address != crowdloan.creator: - print_error( - f"[red]Only the creator can finalize a crowdloan. " - f"Creator: {crowdloan.creator}[/red]" + error_msg = ( + f"Only the creator can finalize a crowdloan. Creator: {crowdloan.creator}" ) + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, "Only the creator can finalize a crowdloan." if crowdloan.finalized: - print_error(f"[red]Crowdloan #{crowdloan_id} is already finalized.[/red]") + error_msg = f"Crowdloan #{crowdloan_id} is already finalized." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, "Crowdloan is already finalized." if crowdloan.raised < crowdloan.cap: - print_error( - f"[red]Crowdloan #{crowdloan_id} has not reached its cap.\n" - f"Raised: {crowdloan.raised}, Cap: {crowdloan.cap}\n" - f"Still needed: {Balance.from_rao(crowdloan.cap.rao - crowdloan.raised.rao)}[/red]" + still_needed = crowdloan.cap - crowdloan.raised + error_msg = ( + f"Crowdloan #{crowdloan_id} has not reached its cap. Raised: {crowdloan.raised.tao}, " + f"Cap: {crowdloan.cap.tao}, Still needed: {still_needed.tao}" ) + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"[red]Crowdloan #{crowdloan_id} has not reached its cap.\n" + f"Raised: {crowdloan.raised}, Cap: {crowdloan.cap}\n" + f"Still needed: {still_needed.tao}[/red]" + ) return False, "Crowdloan has not reached its cap." call = await subtensor.substrate.compose_call( @@ -502,12 +524,24 @@ async def finalize_crowdloan( ) if not Confirm.ask("\nProceed with finalization?"): - console.print("[yellow]Finalization cancelled.[/yellow]") + if json_output: + json_console.print( + json.dumps( + {"success": False, "error": "Finalization cancelled by user."} + ) + ) + else: + console.print("[yellow]Finalization cancelled.[/yellow]") return False, "Finalization cancelled by user." unlock_status = unlock_key(wallet) if not unlock_status.success: - print_error(f"[red]{unlock_status.message}[/red]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": unlock_status.message}) + ) + else: + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( @@ -518,30 +552,56 @@ async def finalize_crowdloan( ) if not success: - print_error( - f"[red]Failed to finalize: {error_message or 'Unknown error'}[/red]" - ) + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": error_message or "Failed to finalize crowdloan.", + } + ) + ) + else: + print_error( + f"[red]Failed to finalize: {error_message or 'Unknown error'}[/red]" + ) return False, error_message or "Failed to finalize crowdloan." - console.print( - f"\n[dark_sea_green3]Successfully finalized crowdloan #{crowdloan_id}![/dark_sea_green3]\n" - ) - - console.print( - f"[bold]Finalization Complete:[/bold]\n" - f"\t• Total Raised: [{COLORS.S.AMOUNT}]{crowdloan.raised}[/{COLORS.S.AMOUNT}]\n" - f"\t• Contributors: {crowdloan.contributors_count}" - ) + if json_output: + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + output_dict = { + "success": True, + "error": None, + "extrinsic_identifier": extrinsic_id, + "data": { + "crowdloan_id": crowdloan_id, + "total_raised": crowdloan.raised.tao, + "contributors_count": crowdloan.contributors_count, + "target_address": crowdloan.target_address, + "has_call": crowdloan.has_call, + "call_executed": crowdloan.has_call, + }, + } + json_console.print(json.dumps(output_dict)) + else: + console.print( + f"\n[dark_sea_green3]Successfully finalized crowdloan #{crowdloan_id}![/dark_sea_green3]\n" + ) - if crowdloan.target_address: console.print( - f"\t• Funds transferred to: [{COLORS.G.SUBHEAD_EX_1}]{crowdloan.target_address}[/{COLORS.G.SUBHEAD_EX_1}]" + f"[bold]Finalization Complete:[/bold]\n" + f"\t• Total Raised: [{COLORS.S.AMOUNT}]{crowdloan.raised}[/{COLORS.S.AMOUNT}]\n" + f"\t• Contributors: {crowdloan.contributors_count}" ) - if crowdloan.has_call: - console.print("\t• [green]Associated call has been executed[/green]") + if crowdloan.target_address: + console.print( + f"\t• Funds transferred to: [{COLORS.G.SUBHEAD_EX_1}]{crowdloan.target_address}[/{COLORS.G.SUBHEAD_EX_1}]" + ) + + if crowdloan.has_call: + console.print("\t• [green]Associated call has been executed[/green]") - if extrinsic_receipt: await print_extrinsic_id(extrinsic_receipt) return True, "Successfully finalized crowdloan." From 17390f06fe4b5f48e0fd466586bef4d21428b0cf Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 15:37:47 -0700 Subject: [PATCH 56/69] wip --- bittensor_cli/src/commands/crowd/contribute.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index 9ea8b19a..480f6a7f 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -267,8 +267,8 @@ async def contribute_to_crowdloan( subtensor.get_single_crowdloan(crowdloan_id), ) - extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() if json_output: + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() output_dict = { "success": True, "error": None, @@ -546,8 +546,8 @@ async def withdraw_from_crowdloan( subtensor.get_single_crowdloan(crowdloan_id), ) - extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() if json_output: + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() output_dict = { "success": True, "error": None, From 2724766c7bf020707b58af887ccd4c677ec68891 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 15:48:31 -0700 Subject: [PATCH 57/69] json output for creation --- bittensor_cli/src/commands/crowd/create.py | 136 ++++++++++++++------- 1 file changed, 94 insertions(+), 42 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 84eeffeb..2c2625b2 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -45,7 +45,12 @@ async def create_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: - print_error(f"[red]{unlock_status.message}[/red]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": unlock_status.message}) + ) + else: + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message crowdloan_type = None @@ -76,8 +81,12 @@ async def create_crowdloan( " • Contributors can withdraw if the cap is not reached\n" ) else: - print_error("Crowdloan type not specified and no prompt provided.") - return False, "Crowdloan type not specified and no prompt provided." + error_msg = "Crowdloan type not specified and no prompt provided." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(error_msg) + return False, error_msg block_hash = await subtensor.substrate.get_chain_head() runtime = await subtensor.substrate.init_runtime(block_hash=block_hash) @@ -107,10 +116,14 @@ async def create_crowdloan( if duration_blocks is None: missing_fields.append("--duration") if missing_fields: - print_error( - "[red]The following options must be provided when prompts are disabled:[/red] " + error_msg = ( + "The following options must be provided when prompts are disabled: " + ", ".join(missing_fields) ) + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, "Missing required options when prompts are disabled." deposit_value = deposit_tao @@ -128,10 +141,11 @@ async def create_crowdloan( ) deposit_value = None continue - print_error( - f"[red]Deposit is below the minimum required deposit " - f"({minimum_deposit.tao:,.4f} TAO).[/red]" - ) + error_msg = f"Deposit is below the minimum required deposit ({minimum_deposit.tao} TAO)." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, "Deposit is below the minimum required deposit." break @@ -325,7 +339,14 @@ async def create_crowdloan( console.print(table) if not Confirm.ask("Proceed with creating the crowdloan?"): - console.print("[yellow]Cancelled crowdloan creation.[/yellow]") + if json_output: + json_console.print( + json.dumps( + {"success": False, "error": "Cancelled crowdloan creation."} + ) + ) + else: + console.print("[yellow]Cancelled crowdloan creation.[/yellow]") return False, "Cancelled crowdloan creation." success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( @@ -335,44 +356,75 @@ async def create_crowdloan( wait_for_finalization=wait_for_finalization, ) - extrinsic_id = None - if extrinsic_receipt: - extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() - if not success: - print_error(f"[red]{error_message or 'Failed to create crowdloan.'}[/red]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": error_message or "Failed to create crowdloan.", + } + ) + ) + else: + print_error(f"[red]{error_message or 'Failed to create crowdloan.'}[/red]") return False, error_message or "Failed to create crowdloan." - if crowdloan_type == "subnet": - message = "Subnet lease crowdloan created successfully." - console.print( - f"\n:white_check_mark: [green]{message}[/green]\n" - f" Type: [magenta]Subnet Leasing[/magenta]\n" - f" Emissions Share: [cyan]{emissions_share}%[/cyan]\n" - f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" - f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" - f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" - f" Ends at block: [bold]{end_block}[/bold]" - ) - if lease_end_block: - console.print(f" Lease ends at block: [bold]{lease_end_block}[/bold]") + if json_output: + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + output_dict = { + "success": True, + "error": None, + "data": { + "type": crowdloan_type, + "deposit": deposit.tao, + "min_contribution": min_contribution.tao, + "cap": cap.tao, + "duration": duration, + "end_block": end_block, + "extrinsic_id": extrinsic_id, + }, + } + + if crowdloan_type == "subnet": + output_dict["data"]["emissions_share"] = emissions_share + output_dict["data"]["lease_end_block"] = lease_end_block + output_dict["data"]["perpetual_lease"] = lease_end_block is None else: - console.print(" Lease: [green]Perpetual[/green]") + output_dict["data"]["target_address"] = target_address + + json_console.print(json.dumps(output_dict)) + message = f"{crowdloan_type.capitalize()} crowdloan created successfully." else: - message = "Fundraising crowdloan created successfully." - console.print( - f"\n:white_check_mark: [green]{message}[/green]\n" - f" Type: [cyan]General Fundraising[/cyan]\n" - f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" - f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" - f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" - f" Ends at block: [bold]{end_block}[/bold]" - ) - if target_address: - console.print(f" Target address: {target_address}") + if crowdloan_type == "subnet": + message = "Subnet lease crowdloan created successfully." + console.print( + f"\n:white_check_mark: [green]{message}[/green]\n" + f" Type: [magenta]Subnet Leasing[/magenta]\n" + f" Emissions Share: [cyan]{emissions_share}%[/cyan]\n" + f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" + f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" + f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" + f" Ends at block: [bold]{end_block}[/bold]" + ) + if lease_end_block: + console.print(f" Lease ends at block: [bold]{lease_end_block}[/bold]") + else: + console.print(" Lease: [green]Perpetual[/green]") + else: + message = "Fundraising crowdloan created successfully." + console.print( + f"\n:white_check_mark: [green]{message}[/green]\n" + f" Type: [cyan]General Fundraising[/cyan]\n" + f" Deposit: [{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]\n" + f" Min contribution: [{COLORS.P.TAO}]{min_contribution}[/{COLORS.P.TAO}]\n" + f" Cap: [{COLORS.P.TAO}]{cap}[/{COLORS.P.TAO}]\n" + f" Ends at block: [bold]{end_block}[/bold]" + ) + if target_address: + console.print(f" Target address: {target_address}") - if extrinsic_id: - console.print(f" Extrinsic ID: [bold]{extrinsic_id}[/bold]") + await print_extrinsic_id(extrinsic_receipt) return True, message From 36e213e704fdc46f5db3cc4eeeef0dbd07345ff4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 15:59:01 -0700 Subject: [PATCH 58/69] update placement --- bittensor_cli/src/commands/crowd/view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index 36d9950b..d496f0aa 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -420,6 +420,10 @@ async def show_crowdloan_details( table.add_row("Address", target_display) + table.add_section() + table.add_row("[cyan underline]CALL DETAILS[/cyan underline]", "") + table.add_section() + has_call_display = ( f"[{COLORS.G.SUCCESS}]Yes[/{COLORS.G.SUCCESS}]" if crowdloan.has_call @@ -428,10 +432,6 @@ async def show_crowdloan_details( table.add_row("Has Call", has_call_display) if crowdloan.has_call and crowdloan.call_details: - table.add_section() - table.add_row("[cyan underline]CALL DETAILS[/cyan underline]", "") - table.add_section() - pallet = crowdloan.call_details.get("pallet", "Unknown") method = crowdloan.call_details.get("method", "Unknown") args = crowdloan.call_details.get("args", {}) From bf31a83758370599595530d799d7091bc68af71b Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 16:08:33 -0700 Subject: [PATCH 59/69] json output for dissolve --- bittensor_cli/cli.py | 4 + bittensor_cli/src/commands/crowd/dissolve.py | 97 ++++++++++++++++---- 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 53024ea8..0ef0b030 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7801,6 +7801,7 @@ def crowd_dissolve( wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Dissolve a crowdloan after all contributors have been refunded. @@ -7813,6 +7814,8 @@ def crowd_dissolve( If there are funds still available other than the creator's contribution, you can run `btcli crowd refund` to refund the remaining contributors. """ + self.verbosity_handler(quiet, verbose, json_output) + if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -7836,6 +7839,7 @@ def crowd_dissolve( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, + json_output=json_output, ) ) diff --git a/bittensor_cli/src/commands/crowd/dissolve.py b/bittensor_cli/src/commands/crowd/dissolve.py index ccb766b0..b7513fb1 100644 --- a/bittensor_cli/src/commands/crowd/dissolve.py +++ b/bittensor_cli/src/commands/crowd/dissolve.py @@ -1,4 +1,5 @@ import asyncio +import json from bittensor_wallet import Wallet from rich.prompt import Confirm @@ -10,6 +11,7 @@ from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, + json_console, print_extrinsic_id, print_error, unlock_key, @@ -23,6 +25,7 @@ async def dissolve_crowdloan( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = True, + json_output: bool = False, ) -> tuple[bool, str]: """Dissolve a non-finalized crowdloan after refunding contributors. @@ -49,21 +52,33 @@ async def dissolve_crowdloan( ) if not crowdloan: - print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") - return False, f"Crowdloan #{crowdloan_id} not found." + error_msg = f"Crowdloan #{crowdloan_id} not found." + 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.finalized: - print_error( - f"[red]Crowdloan #{crowdloan_id} is already finalized and cannot be dissolved.[/red]" + error_msg = ( + f"Crowdloan #{crowdloan_id} is already finalized and cannot be dissolved." ) + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, f"Crowdloan #{crowdloan_id} is finalized." if creator_ss58 != crowdloan.creator: - print_error( - f"[red]Only the creator can dissolve this crowdloan.[/red]\n" - f"Creator: [blue]{crowdloan.creator}[/blue]\n" - f"Your address: [blue]{creator_ss58}[/blue]" - ) + error_msg = f"Only the creator can dissolve this crowdloan. Creator: {crowdloan.creator}, Your address: {creator_ss58}" + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"[red]Only the creator can dissolve this crowdloan.[/red]\n" + f"Creator: [blue]{crowdloan.creator}[/blue]\n" + f"Your address: [blue]{creator_ss58}[/blue]" + ) return False, "Only the creator can dissolve this crowdloan." creator_contribution = await subtensor.get_crowdloan_contribution( @@ -71,12 +86,20 @@ async def dissolve_crowdloan( ) if creator_contribution != crowdloan.raised: - print_error( - f"[red]Crowdloan still holds funds from other contributors.[/red]\n" - f"Raised amount: [yellow]{crowdloan.raised}[/yellow]\n" - f"Creator's contribution: [yellow]{creator_contribution}[/yellow]\n" - "Run [cyan]btcli crowd refund[/cyan] until only the creator's funds remain." + error_msg = ( + f"Crowdloan still holds funds from other contributors. " + f"Raised: {crowdloan.raised.tao}, Creator's contribution: {creator_contribution.tao}. " + "Run 'btcli crowd refund' until only the creator's funds remain." ) + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"[red]Crowdloan still holds funds from other contributors.[/red]\n" + f"Raised amount: [yellow]{crowdloan.raised}[/yellow]\n" + f"Creator's contribution: [yellow]{creator_contribution}[/yellow]\n" + "Run [cyan]btcli crowd refund[/cyan] until only the creator's funds remain." + ) return False, "Crowdloan not ready to dissolve." await show_crowdloan_details( @@ -114,12 +137,24 @@ async def dissolve_crowdloan( f"\n[bold]Proceed with dissolving crowdloan #{crowdloan_id}?[/bold]", default=False, ): - console.print("[yellow]Dissolution cancelled.[/yellow]") + if json_output: + json_console.print( + json.dumps( + {"success": False, "error": "Dissolution cancelled by user."} + ) + ) + else: + console.print("[yellow]Dissolution cancelled.[/yellow]") return False, "Dissolution cancelled by user." unlock_status = unlock_key(wallet) if not unlock_status.success: - print_error(f"[red]{unlock_status.message}[/red]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": unlock_status.message}) + ) + else: + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message with console.status( @@ -142,12 +177,34 @@ async def dissolve_crowdloan( ) if not success: - print_error(f"[red]Failed to dissolve crowdloan.[/red]\n{error_message}") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": error_message or "Failed to dissolve crowdloan.", + } + ) + ) + else: + print_error(f"[red]Failed to dissolve crowdloan.[/red]\n{error_message}") return False, error_message - if extrinsic_receipt: + if json_output: + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + output_dict = { + "success": True, + "error": None, + "extrinsic_identifier": extrinsic_id, + "data": { + "crowdloan_id": crowdloan_id, + "creator": crowdloan.creator, + "total_dissolved": creator_contribution.tao, + }, + } + json_console.print(json.dumps(output_dict)) + else: await print_extrinsic_id(extrinsic_receipt) - - console.print("[green]Crowdloan dissolved successfully![/green]") + console.print("[green]Crowdloan dissolved successfully![/green]") return True, "Crowdloan dissolved successfully." From cd38ddad338850646f1fc902ec13f4e61fcc88cf Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 16:16:55 -0700 Subject: [PATCH 60/69] json for refund --- bittensor_cli/src/commands/crowd/refund.py | 86 +++++++++++++++++----- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/refund.py b/bittensor_cli/src/commands/crowd/refund.py index dc42f388..edbe32fb 100644 --- a/bittensor_cli/src/commands/crowd/refund.py +++ b/bittensor_cli/src/commands/crowd/refund.py @@ -1,4 +1,5 @@ import asyncio +import json from bittensor_wallet import Wallet from rich.prompt import Confirm @@ -9,6 +10,7 @@ from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, + json_console, print_extrinsic_id, print_error, unlock_key, @@ -24,6 +26,7 @@ async def refund_crowdloan( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = True, + json_output: bool = False, ) -> tuple[bool, str]: """Refund contributors of a non-finalized crowdloan. @@ -51,20 +54,34 @@ async def refund_crowdloan( ) if not crowdloan: - print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") - return False, f"Crowdloan #{crowdloan_id} not found." + error_msg = f"Crowdloan #{crowdloan_id} not found." + 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.finalized: - print_error( - f"[red]Crowdloan #{crowdloan_id} is already finalized. " - "Finalized crowdloans cannot be refunded.[/red]" - ) + error_msg = f"Crowdloan #{crowdloan_id} is already finalized. Finalized crowdloans cannot be refunded." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, f"Crowdloan #{crowdloan_id} is already finalized." + if crowdloan.end > current_block: - print_error( - f"[red]Crowdloan #{crowdloan_id} is not yet ended. " - f"End block: [cyan]{crowdloan.end:,}[/cyan] ([dim]{blocks_to_duration(crowdloan.end - current_block)} remaining[/dim])[/red]" + time_remaining = blocks_to_duration(crowdloan.end - current_block) + error_msg = ( + f"Crowdloan #{crowdloan_id} is not yet ended. " + f"End block: {crowdloan.end} ({time_remaining} remaining)" ) + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"Crowdloan #{crowdloan_id} is not yet ended. " + f"End block: [cyan]{crowdloan.end}[/cyan] ([dim]{time_remaining} remaining[/dim])" + ) return False, f"Crowdloan #{crowdloan_id} is not yet ended." await show_crowdloan_details( @@ -133,12 +150,22 @@ async def refund_crowdloan( f"\n[bold]Proceed with refunding contributors of Crowdloan #{crowdloan_id}?[/bold]", default=False, ): - console.print("[yellow]Refund cancelled.[/yellow]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": "Refund cancelled by user."}) + ) + else: + console.print("[yellow]Refund cancelled.[/yellow]") return False, "Refund cancelled by user." unlock_status = unlock_key(wallet) if not unlock_status.success: - print_error(f"[red]{unlock_status.message}[/red]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": unlock_status.message}) + ) + else: + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message with console.status( @@ -163,13 +190,38 @@ async def refund_crowdloan( ) if not success: - print_error(f"[red]Failed to refund contributors.[/red]\n{error_message}") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": error_message or "Failed to refund contributors.", + } + ) + ) + else: + print_error(f"[red]Failed to refund contributors.[/red]\n{error_message}") return False, error_message - console.print( - f"[green]Contributors have been refunded for Crowdloan #{crowdloan_id}.[/green]" - ) - if extrinsic_receipt: + if json_output: + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + output_dict = { + "success": True, + "error": None, + "extrinsic_identifier": extrinsic_id, + "data": { + "crowdloan_id": crowdloan_id, + "refund_limit_per_call": refund_limit, + "total_contributors": crowdloan.contributors_count, + "estimated_calls_remaining": max(0, estimated_calls - 1), + "amount_refunded": (crowdloan.raised - crowdloan.deposit).tao, + }, + } + json_console.print(json.dumps(output_dict)) + else: + console.print( + f"[green]Contributors have been refunded for Crowdloan #{crowdloan_id}.[/green]" + ) await print_extrinsic_id(extrinsic_receipt) - return True, "Contributors have been refunded for Crowdloan #{crowdloan_id}." + return True, f"Contributors have been refunded for Crowdloan #{crowdloan_id}." From babbcf6b0feab5150551d6fbbc994dcbd275ac18 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 16:24:29 -0700 Subject: [PATCH 61/69] json to update --- bittensor_cli/src/commands/crowd/update.py | 164 +++++++++++++++------ 1 file changed, 120 insertions(+), 44 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py index 2c6e757f..2b2ee04f 100644 --- a/bittensor_cli/src/commands/crowd/update.py +++ b/bittensor_cli/src/commands/crowd/update.py @@ -1,4 +1,5 @@ import asyncio +import json from typing import Optional from bittensor_wallet import Wallet @@ -11,6 +12,7 @@ from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, + json_console, print_error, unlock_key, print_extrinsic_id, @@ -29,6 +31,7 @@ async def update_crowdloan( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = True, + json_output: bool = False, ) -> tuple[bool, str]: """Update parameters of a non-finalized crowdloan. @@ -62,23 +65,35 @@ async def update_crowdloan( absolute_min = Balance.from_rao(absolute_min_rao) if not crowdloan: - print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") - return False, f"Crowdloan #{crowdloan_id} not found." + error_msg = f"Crowdloan #{crowdloan_id} not found." + 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.finalized: - print_error( - f"[red]Crowdloan #{crowdloan_id} is already finalized and cannot be updated.[/red]" + error_msg = ( + f"Crowdloan #{crowdloan_id} is already finalized and cannot be updated." ) + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error(f"[red]{error_msg}[/red]") return False, f"Crowdloan #{crowdloan_id} is already finalized." creator_address = wallet.coldkeypub.ss58_address if creator_address != crowdloan.creator: - print_error( - f"[red]Only the creator can update this crowdloan.[/red]\n" - f"Creator: [blue]{crowdloan.creator}[/blue]\n" - f"Your address: [blue]{creator_address}[/blue]" - ) - return False, "Only the creator can update this crowdloan." + error_msg = "Only the creator can update this crowdloan." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"[red]Only the creator can update this crowdloan.[/red]\n" + f"Creator: [blue]{crowdloan.creator}[/blue]\n" + f"Your address: [blue]{creator_address}[/blue]" + ) + return False, error_msg await show_crowdloan_details( subtensor=subtensor, @@ -105,7 +120,12 @@ async def update_crowdloan( ) if choice == 4: - console.print("[yellow]Update cancelled.[/yellow]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": "Update cancelled by user."}) + ) + else: + console.print("[yellow]Update cancelled.[/yellow]") return False, "Update cancelled by user." if choice == 1: @@ -215,45 +235,69 @@ async def update_crowdloan( update_type = "End Block" if call_function is None or value is None or param_name is None: - print_error("[red]No update parameter specified.[/red]") - return False, "No update parameter specified." + error_msg = "No update parameter specified." + 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 # Validation if call_function == "update_min_contribution": if value.rao < absolute_min.rao: - print_error( - f"[red]Minimum contribution ({value}) must be at least {absolute_min}.[/red]" - ) - return False, f"Minimum contribution must be at least {absolute_min}." + error_msg = f"Minimum contribution must be at least {absolute_min}." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"[red]Minimum contribution ({value}) must be at least {absolute_min}.[/red]" + ) + return False, error_msg elif call_function == "update_end": if value <= current_block: - print_error( - f"[red]End block ({value:,}) must be after current block ({current_block:,}).[/red]" - ) - return False, "End block must be in the future." + error_msg = "End block must be in the future." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"[red]End block ({value:,}) must be after current block ({current_block:,}).[/red]" + ) + return False, error_msg block_duration = value - current_block if block_duration < min_duration: - print_error( - f"[red]Duration ({blocks_to_duration(block_duration)}) is too short. " - f"Minimum: [dim]{min_end_block} - {blocks_to_duration(min_duration)}[/dim][/red]" - ) - return False, "Block duration too short." + error_msg = "Block duration too short." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"[red]Duration ({blocks_to_duration(block_duration)}) is too short. " + f"Minimum: [dim]{min_end_block} - {blocks_to_duration(min_duration)}[/dim][/red]" + ) + return False, error_msg if block_duration > max_duration: - print_error( - f"[red]Duration ({blocks_to_duration(block_duration)}) is too long. " - f"Maximum: [dim]{max_end_block} - {blocks_to_duration(max_duration)}[/dim][/red]" - ) - return False, "Block duration too long." + error_msg = "Block duration too long." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"[red]Duration ({blocks_to_duration(block_duration)}) is too long. " + f"Maximum: [dim]{max_end_block} - {blocks_to_duration(max_duration)}[/dim][/red]" + ) + return False, error_msg elif call_function == "update_cap": if value < crowdloan.raised: - print_error( - f"[red]New cap ({value}) must be at least the amount already raised ({crowdloan.raised}).[/red]" - ) - return False, "Cap must be >= raised amount." + error_msg = "Cap must be >= raised amount." + if json_output: + json_console.print(json.dumps({"success": False, "error": error_msg})) + else: + print_error( + f"[red]New cap ({value}) must be at least the amount already raised ({crowdloan.raised}).[/red]" + ) + return False, error_msg # Update summary table = Table( @@ -287,15 +331,25 @@ async def update_crowdloan( if prompt and not Confirm.ask( f"\n[bold]Proceed with updating {update_type}?[/bold]", default=False ): - console.print("[yellow]Update cancelled.[/yellow]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": "Update cancelled by user."}) + ) + else: + console.print("[yellow]Update cancelled.[/yellow]") return False, "Update cancelled by user." unlock_status = unlock_key(wallet) if not unlock_status.success: - print_error(f"[red]{unlock_status.message}[/red]") + if json_output: + json_console.print( + json.dumps({"success": False, "error": unlock_status.message}) + ) + else: + print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message - if call_function == "update_min_contribution": + if call_function != "update_end": value = value.rao with console.status( @@ -319,14 +373,36 @@ async def update_crowdloan( ) if not success: - print_error(f"[red]Failed to update {update_type}.[/red]\n{error_message}") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": error_message or f"Failed to update {update_type}.", + } + ) + ) + else: + print_error(f"[red]Failed to update {update_type}.[/red]\n{error_message}") return False, error_message - console.print( - f"[green]{update_type} updated successfully![/green]\n" - f"Crowdloan #{crowdloan_id} has been updated." - ) - if extrinsic_receipt: + if json_output: + extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier() + output_dict = { + "success": True, + "error": None, + "extrinsic_identifier": extrinsic_id, + "data": { + "crowdloan_id": crowdloan_id, + "update_type": update_type, + }, + } + json_console.print(json.dumps(output_dict)) + else: + console.print( + f"[green]{update_type} updated successfully![/green]\n" + f"Crowdloan #{crowdloan_id} has been updated." + ) await print_extrinsic_id(extrinsic_receipt) return True, f"{update_type} updated successfully." From 6eb63cf2540fe8f3a221f0626c8c91bdfe132804 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 16:24:41 -0700 Subject: [PATCH 62/69] update cli --- bittensor_cli/cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0ef0b030..39f6c5f6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7686,6 +7686,7 @@ def crowd_update( wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Update one mutable field on a non-finalized crowdloan. @@ -7696,6 +7697,8 @@ def crowd_update( against the chain constants (absolute minimum contribution, block-duration bounds, etc.). """ + self.verbosity_handler(quiet, verbose, json_output) + if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -7727,6 +7730,7 @@ def crowd_update( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, + json_output=json_output, ) ) @@ -7748,6 +7752,7 @@ def crowd_refund( wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Refund contributors of a non-finalized crowdloan. @@ -7757,6 +7762,8 @@ def crowd_refund( 50) excluding the creator. Run it repeatedly until everyone except the creator has been reimbursed. """ + self.verbosity_handler(quiet, verbose, json_output) + if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -7780,6 +7787,7 @@ def crowd_refund( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, + json_output=json_output, ) ) From 44289aae5c1805602e2cb934b0fd23d1ee4b8955 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 16:35:39 -0700 Subject: [PATCH 63/69] json output for list + info --- bittensor_cli/src/commands/crowd/view.py | 178 ++++++++++++++++++++++- 1 file changed, 174 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index d496f0aa..ba7657dd 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -1,6 +1,7 @@ from typing import Optional import asyncio +import json from bittensor_wallet import Wallet from rich import box from rich.table import Column, Table @@ -12,6 +13,7 @@ from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, + json_console, print_error, millify_tao, ) @@ -45,15 +47,33 @@ def _time_remaining(loan: CrowdloanData, current_block: int) -> str: async def list_crowdloans( subtensor: SubtensorInterface, verbose: bool = False, + json_output: bool = False, ) -> bool: - """List all crowdloans in a tabular format.""" + """List all crowdloans in a tabular format or JSON output.""" current_block, loans = await asyncio.gather( subtensor.substrate.get_block_number(None), subtensor.get_crowdloans(), ) if not loans: - console.print("[yellow]No crowdloans found.[/yellow]") + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "error": None, + "data": { + "crowdloans": [], + "total_count": 0, + "total_raised": 0, + "total_cap": 0, + "total_contributors": 0, + }, + } + ) + ) + else: + console.print("[yellow]No crowdloans found.[/yellow]") return True total_raised = sum(loan.raised.tao for loan in loans.values()) @@ -67,6 +87,69 @@ async def list_crowdloans( f"[{percentage_color}]{funding_percentage:.2f}%[/{percentage_color}]" ) + if json_output: + crowdloans_list = [] + for loan_id, loan in loans.items(): + status = _status(loan, current_block) + time_remaining = _time_remaining(loan, current_block) + + call_info = None + if loan.call_details: + pallet = loan.call_details.get("pallet", "") + method = loan.call_details.get("method", "") + if pallet == "SubtensorModule" and method == "register_leased_network": + call_info = "Subnet Leasing" + else: + call_info = ( + f"{pallet}.{method}" + if pallet and method + else method or pallet or "Unknown" + ) + elif loan.has_call: + call_info = "Unknown" + + crowdloan_data = { + "id": loan_id, + "status": status, + "raised": loan.raised.tao, + "cap": loan.cap.tao, + "deposit": loan.deposit.tao, + "min_contribution": loan.min_contribution.tao, + "end_block": loan.end, + "time_remaining": time_remaining, + "contributors_count": loan.contributors_count, + "creator": loan.creator, + "target_address": loan.target_address, + "funds_account": loan.funds_account, + "call": call_info, + "finalized": loan.finalized, + } + crowdloans_list.append(crowdloan_data) + + crowdloans_list.sort( + key=lambda x: ( + x["status"] != "Active", + -x["raised"], + ) + ) + + output_dict = { + "success": True, + "error": None, + "data": { + "crowdloans": crowdloans_list, + "total_count": total_loans, + "total_raised": total_raised, + "total_cap": total_cap, + "total_contributors": total_contributors, + "funding_percentage": funding_percentage, + "current_block": current_block, + "network": subtensor.network, + }, + } + json_console.print(json.dumps(output_dict)) + return True + if not verbose: funding_string = f"τ {millify_tao(total_raised)}/{millify_tao(total_cap)} ({formatted_percentage})" else: @@ -243,6 +326,7 @@ async def show_crowdloan_details( current_block: Optional[int] = None, wallet: Optional[Wallet] = None, verbose: bool = False, + json_output: bool = False, ) -> tuple[bool, str]: """Display detailed information about a specific crowdloan.""" @@ -252,8 +336,12 @@ async def show_crowdloan_details( subtensor.get_single_crowdloan(crowdloan_id), ) if not crowdloan: - print_error(f"[red]Crowdloan #{crowdloan_id} not found.[/red]") - return False, f"Crowdloan #{crowdloan_id} not found." + error_msg = f"Crowdloan #{crowdloan_id} not found." + 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 user_contribution = None if wallet and wallet.coldkeypub: @@ -270,6 +358,88 @@ async def show_crowdloan_details( } status_color = status_color_map.get(status, "white") + if json_output: + time_remaining = _time_remaining(crowdloan, current_block) + + avg_contribution = None + if crowdloan.contributors_count > 0: + net_contributions = crowdloan.raised.tao - crowdloan.deposit.tao + avg_contribution = ( + net_contributions / (crowdloan.contributors_count - 1) + if crowdloan.contributors_count > 1 + else crowdloan.deposit.tao + ) + + call_info = None + if crowdloan.has_call and crowdloan.call_details: + pallet = crowdloan.call_details.get("pallet", "Unknown") + method = crowdloan.call_details.get("method", "Unknown") + args = crowdloan.call_details.get("args", {}) + + if pallet == "SubtensorModule" and method == "register_leased_network": + call_info = { + "type": "Subnet Leasing", + "pallet": pallet, + "method": method, + "emissions_share": args.get("emissions_share", {}).get("value"), + "end_block": args.get("end_block", {}).get("value"), + } + else: + call_info = {"pallet": pallet, "method": method, "args": args} + + user_contribution_info = None + if user_contribution: + is_creator = ( + wallet + and wallet.coldkeypub + and wallet.coldkeypub.ss58_address == crowdloan.creator + ) + withdrawable_amount = None + + if status == "Active" and not crowdloan.finalized: + if is_creator and user_contribution.tao > crowdloan.deposit.tao: + withdrawable_amount = user_contribution.tao - crowdloan.deposit.tao + elif not is_creator: + withdrawable_amount = user_contribution.tao + + user_contribution_info = { + "amount": user_contribution.tao, + "is_creator": is_creator, + "withdrawable": withdrawable_amount, + "refundable": status == "Closed", + } + + output_dict = { + "success": True, + "error": None, + "data": { + "crowdloan_id": crowdloan_id, + "status": status, + "finalized": crowdloan.finalized, + "creator": crowdloan.creator, + "funds_account": crowdloan.funds_account, + "raised": crowdloan.raised.tao, + "cap": crowdloan.cap.tao, + "raised_percentage": (crowdloan.raised.tao / crowdloan.cap.tao * 100) + if crowdloan.cap.tao > 0 + else 0, + "deposit": crowdloan.deposit.tao, + "min_contribution": crowdloan.min_contribution.tao, + "end_block": crowdloan.end, + "current_block": current_block, + "time_remaining": time_remaining, + "contributors_count": crowdloan.contributors_count, + "average_contribution": avg_contribution, + "target_address": crowdloan.target_address, + "has_call": crowdloan.has_call, + "call_details": call_info, + "user_contribution": user_contribution_info, + "network": subtensor.network, + }, + } + json_console.print(json.dumps(output_dict)) + return True, f"Displayed info for crowdloan #{crowdloan_id}" + table = Table( Column( "Field", From c5a0c02ce12fb1c6e0c78d649a8c6232679f9c11 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 16:35:59 -0700 Subject: [PATCH 64/69] update cli --- bittensor_cli/cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 39f6c5f6..5f94fe6d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7299,11 +7299,12 @@ def crowd_list( [green]$[/green] btcli crowd list --verbose """ - self.verbosity_handler(quiet, verbose, False) + self.verbosity_handler(quiet, verbose, json_output) return self._run_command( view_crowdloan.list_crowdloans( subtensor=self.initialize_chain(network), verbose=verbose, + json_output=json_output, ) ) @@ -7335,7 +7336,7 @@ def crowd_info( [green]$[/green] btcli crowd info --id 1 --verbose """ - self.verbosity_handler(quiet, verbose, False) + self.verbosity_handler(quiet, verbose, json_output) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -7360,6 +7361,7 @@ def crowd_info( crowdloan_id=crowdloan_id, wallet=wallet, verbose=verbose, + json_output=json_output, ) ) From 1df4e14b39361999a7e00a69612072aa347cc55c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 16:51:25 -0700 Subject: [PATCH 65/69] use walrus --- bittensor_cli/src/bittensor/chain_data.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 5c3f3639..cfcc699f 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1232,15 +1232,19 @@ class CrowdloanData(InfoBase): @classmethod def _fix_decoded(cls, decoded: dict[str, Any]) -> "CrowdloanData": - creator = decode_account_id(decoded["creator"]) if decoded["creator"] else None + creator = ( + decode_account_id(creator_raw) + if (creator_raw := decoded.get("creator")) + else None + ) funds_account = ( - decode_account_id(decoded["funds_account"]) - if decoded["funds_account"] + decode_account_id(funds_raw) + if (funds_raw := decoded.get("funds_account")) else None ) target_address = ( - decode_account_id(decoded["target_address"]) - if decoded["target_address"] + decode_account_id(target_raw) + if (target_raw := decoded.get("target_address")) else None ) return cls( From d11d56314000c18fc452d9cf70f6545a16c97cb6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 16 Oct 2025 16:52:45 -0700 Subject: [PATCH 66/69] update refund check --- bittensor_cli/src/commands/crowd/refund.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/refund.py b/bittensor_cli/src/commands/crowd/refund.py index edbe32fb..d08f9129 100644 --- a/bittensor_cli/src/commands/crowd/refund.py +++ b/bittensor_cli/src/commands/crowd/refund.py @@ -48,6 +48,7 @@ async def refund_crowdloan( Returns: tuple[bool, str]: Success status and message """ + creator_ss58 = wallet.coldkeypub.ss58_address crowdloan, current_block = await asyncio.gather( subtensor.get_single_crowdloan(crowdloan_id), subtensor.substrate.get_block_number(None), @@ -69,20 +70,17 @@ async def refund_crowdloan( print_error(f"[red]{error_msg}[/red]") return False, f"Crowdloan #{crowdloan_id} is already finalized." - if crowdloan.end > current_block: - time_remaining = blocks_to_duration(crowdloan.end - current_block) - error_msg = ( - f"Crowdloan #{crowdloan_id} is not yet ended. " - f"End block: {crowdloan.end} ({time_remaining} remaining)" - ) + if creator_ss58 != crowdloan.creator: + error_msg = f"Only the creator can refund this crowdloan. Creator: {crowdloan.creator}, Your address: {creator_ss58}" if json_output: json_console.print(json.dumps({"success": False, "error": error_msg})) else: print_error( - f"Crowdloan #{crowdloan_id} is not yet ended. " - f"End block: [cyan]{crowdloan.end}[/cyan] ([dim]{time_remaining} remaining[/dim])" + f"[red]Only the creator can refund this crowdloan.[/red]\n" + f"Creator: [blue]{crowdloan.creator}[/blue]\n" + f"Your address: [blue]{creator_ss58}[/blue]" ) - return False, f"Crowdloan #{crowdloan_id} is not yet ended." + return False, "Only the creator can refund this crowdloan." await show_crowdloan_details( subtensor=subtensor, From 789aa020017e5ba656e75b4753112e12ca360284 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 20 Oct 2025 15:00:53 +0200 Subject: [PATCH 67/69] Fixes the double `--hotkey` arg in set childkey take --- bittensor_cli/cli.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a73aa2e8..591ca3f4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5178,7 +5178,12 @@ def stake_childkey_take( wallet_hotkey: Optional[str] = Options.wallet_hotkey, wallet_path: Optional[str] = Options.wallet_path, network: Optional[list[str]] = Options.network, - hotkey: Optional[str] = None, + child_hotkey_ss58: Optional[str] = typer.Option( + None, + "child-hotkey-ss58", + help="The hotkey SS58 to designate as child (not specifying will use the provided wallet's hotkey)", + prompt=False, + ), netuid: Optional[int] = typer.Option( None, help="The netuid of the subnet, (e.g. 23)", @@ -5215,11 +5220,11 @@ def stake_childkey_take( To get the current take value, do not use the '--take' option: - [green]$[/green] btcli stake child take --hotkey --netuid 1 + [green]$[/green] btcli stake child take --child-hotkey-ss58 --netuid 1 To set a new take value, use the '--take' option: - [green]$[/green] btcli stake child take --hotkey --take 0.12 --netuid 1 + [green]$[/green] btcli stake child take --child-hotkey-ss58 --take 0.12 --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( From 07626c3ac02b2b43c0bb8f0d30c6fa2bfa19b273 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 20 Oct 2025 15:15:57 +0200 Subject: [PATCH 68/69] Updates the help text for coldkey regen --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a73aa2e8..130d0704 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2480,7 +2480,7 @@ def wallet_regen_coldkey( """ Regenerate a coldkey for a wallet on the Bittensor blockchain network. - This command is used to create a new coldkey from an existing mnemonic, seed, or JSON file. + This command is used to create a new instance of a coldkey from an existing mnemonic, seed, or JSON file. USAGE From 625707a70b19a640562e16475af1e1e6a6b245fa Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 20 Oct 2025 19:30:01 +0200 Subject: [PATCH 69/69] Changelog + version --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d32aab8..90f96f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ # Changelog +## 9.14.0 /2025-10-20 +* Skips senate tests by @thewhaleking in https://github.com/opentensor/btcli/pull/658 +* Feat/crowdloans by @ibraheem-abe in https://github.com/opentensor/btcli/pull/657 +* Removes the fetching of identities in GitHub by @thewhaleking in https://github.com/opentensor/btcli/pull/659 +* Fixes the double `--hotkey` arg in set childkey take by @thewhaleking in https://github.com/opentensor/btcli/pull/662 +* Updates the help text for coldkey regen by @thewhaleking in https://github.com/opentensor/btcli/pull/663 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.13.1...v9.14.0 # 9.13.1 /2025-10-14 * Fix for complicated (user_liquidity_enabled) hyperparams by @thewhaleking in https://github.com/opentensor/btcli/pull/652 diff --git a/pyproject.toml b/pyproject.toml index ee4309dc..a9f725de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.13.1" +version = "9.14.0" description = "Bittensor CLI" readme = "README.md" authors = [