Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8839,6 +8839,22 @@ def crowd_create(
help="Block number when subnet lease ends (omit for perpetual lease).",
min=1,
),
call_module: Optional[str] = typer.Option(
None,
"--call-module",
help="Custom call pallet/module name to attach to the crowdloan.",
),
call_function: Optional[str] = typer.Option(
None,
"--call-function",
"--call-method",
help="Custom call function/method name to attach to the crowdloan.",
),
call_args: Optional[str] = typer.Option(
None,
"--call-args",
help="JSON object of call arguments for the custom call (e.g. '{\"netuid\": 1}').",
),
prompt: bool = Options.prompt,
wait_for_inclusion: bool = Options.wait_for_inclusion,
wait_for_finalization: bool = Options.wait_for_finalization,
Expand Down Expand Up @@ -8886,6 +8902,9 @@ def crowd_create(
subnet_lease=subnet_lease,
emissions_share=emissions_share,
lease_end_block=lease_end_block,
call_module=call_module,
call_function=call_function,
call_args=call_args,
wait_for_inclusion=wait_for_inclusion,
wait_for_finalization=wait_for_finalization,
prompt=prompt,
Expand Down
38 changes: 38 additions & 0 deletions bittensor_cli/src/bittensor/subtensor_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1925,6 +1925,44 @@ async def get_crowdloan_contribution(
return Balance.from_rao(contribution)
return None

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

Args:
crowdloan_id: The ID of the crowdloan.
block_hash: Optional block hash to query against.
reuse_block: Whether to reuse the last-used block hash.

Returns:
List of (contributor_ss58, contribution_balance) tuples.
"""
result = await self.substrate.query_map(
module="Crowdloan",
storage_function="Contributions",
params=[crowdloan_id],
block_hash=block_hash,
reuse_block_hash=reuse_block,
fully_exhaust=True,
)
contributors: list[tuple[str, Balance]] = []
async for key, value in result:
# For double-map storage, the contributor is the remaining key after crowdloan_id.
contributor_key = key[0] if isinstance(key, (list, tuple)) else key
contributor_ss58 = decode_account_id(contributor_key)
amount = (
Balance.from_rao(value.value)
if hasattr(value, "value")
else Balance.from_rao(value)
)
contributors.append((contributor_ss58, amount))
return contributors

async def get_coldkey_swap_schedule_duration(
self,
block_hash: Optional[str] = None,
Expand Down
56 changes: 56 additions & 0 deletions bittensor_cli/src/commands/crowd/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ async def create_crowdloan(
subnet_lease: Optional[bool],
emissions_share: Optional[int],
lease_end_block: Optional[int],
call_module: Optional[str],
call_function: Optional[str],
call_args: Optional[str],
wait_for_inclusion: bool,
wait_for_finalization: bool,
prompt: bool,
Expand Down Expand Up @@ -273,6 +276,51 @@ async def create_crowdloan(

call_to_attach = None

if call_module or call_function or call_args:
if not call_module or not call_function:
error_msg = "Both --call-module and --call-function are required for custom calls."
if json_output:
json_console.print(json.dumps({"success": False, "error": error_msg}))
else:
print_error(f"[red]{error_msg}[/red]")
return False, error_msg
if crowdloan_type == "subnet":
error_msg = "Custom calls cannot be combined with subnet lease crowdloans."
if json_output:
json_console.print(json.dumps({"success": False, "error": error_msg}))
else:
print_error(f"[red]{error_msg}[/red]")
return False, error_msg
try:
call_params = json.loads(call_args) if call_args else {}
except json.JSONDecodeError as exc:
error_msg = f"Invalid JSON for --call-args: {exc}"
if json_output:
json_console.print(json.dumps({"success": False, "error": error_msg}))
else:
print_error(f"[red]{error_msg}[/red]")
return False, error_msg
if call_params and not isinstance(call_params, dict):
error_msg = "--call-args must be a JSON object."
if json_output:
json_console.print(json.dumps({"success": False, "error": error_msg}))
else:
print_error(f"[red]{error_msg}[/red]")
return False, error_msg
try:
call_to_attach = await subtensor.substrate.compose_call(
call_module=call_module,
call_function=call_function,
call_params=call_params,
)
except Exception as exc:
error_msg = f"Failed to compose custom call: {exc}"
if json_output:
json_console.print(json.dumps({"success": False, "error": error_msg}))
else:
print_error(f"[red]{error_msg}[/red]")
return False, error_msg

creator_balance = await subtensor.get_balance(
proxy or wallet.coldkeypub.ss58_address
)
Expand Down Expand Up @@ -336,6 +384,8 @@ async def create_crowdloan(
else f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]"
)
table.add_row("Target address", target_text)
if call_module and call_function:
table.add_row("Custom Call", f"{call_module}.{call_function}")

table.add_row("Deposit", f"[{COLORS.P.TAO}]{deposit}[/{COLORS.P.TAO}]")
table.add_row(
Expand Down Expand Up @@ -408,6 +458,12 @@ async def create_crowdloan(
output_dict["data"]["perpetual_lease"] = lease_end_block is None
else:
output_dict["data"]["target_address"] = target_address
if call_module and call_function:
output_dict["data"]["call"] = {
"pallet": call_module,
"method": call_function,
"args": json.loads(call_args) if call_args else {},
}

json_console.print(json.dumps(output_dict))
message = f"{crowdloan_type.capitalize()} crowdloan created successfully."
Expand Down
54 changes: 54 additions & 0 deletions bittensor_cli/src/commands/crowd/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def _shorten(account: Optional[str]) -> str:
return f"{account[:6]}…{account[-6:]}"


def _identity_label(account: str, identities: dict[str, dict]) -> str:
identity = identities.get(account, {})
return identity.get("name") or identity.get("display") or _shorten(account)


def _status(loan: CrowdloanData, current_block: int) -> str:
if loan.finalized:
return "Finalized"
Expand Down Expand Up @@ -358,6 +363,31 @@ async def show_crowdloan_details(
}
status_color = status_color_map.get(status, "white")

contributors: list[tuple[str, Balance]] = []
contributors_view: list[dict[str, object]] = []
if crowdloan.contributors_count > 0:
status_msg = "Fetching contributors and identities..." if not json_output else None
status_ctx = console.status(status_msg) if status_msg else None
try:
if status_ctx:
status_ctx.__enter__()
contributors, identities = await asyncio.gather(
subtensor.get_crowdloan_contributors(crowdloan_id),
subtensor.query_all_identities(),
)
contributors_view = [
{
"address": address,
"identity": _identity_label(address, identities),
"amount": contribution.tao,
}
for address, contribution in contributors
]
contributors_view.sort(key=lambda x: x["amount"], reverse=True)
finally:
if status_ctx:
status_ctx.__exit__(None, None, None)

if json_output:
time_remaining = _time_remaining(crowdloan, current_block)

Expand Down Expand Up @@ -429,6 +459,7 @@ async def show_crowdloan_details(
"current_block": current_block,
"time_remaining": time_remaining,
"contributors_count": crowdloan.contributors_count,
"contributors": contributors_view,
"average_contribution": avg_contribution,
"target_address": crowdloan.target_address,
"has_call": crowdloan.has_call,
Expand Down Expand Up @@ -638,4 +669,27 @@ async def show_crowdloan_details(
table.add_row(arg_name, str(display_value))

console.print(table)
if contributors_view:
contrib_table = Table(
Column("Contributor", style=COLORS.G.SUBHEAD, no_wrap=True),
Column("Identity", style=COLORS.G.TEMPO),
Column(f"Contribution ({Balance.get_unit(0)})", style=COLORS.P.TAO),
title=f"[{COLORS.G.HEADER}]Contributors[/]",
show_header=True,
show_edge=False,
box=box.SIMPLE,
border_style="bright_black",
)
for entry in contributors_view:
amount = (
f"τ {entry['amount']:,.4f}"
if verbose
else f"τ {millify_tao(entry['amount'])}"
)
contrib_table.add_row(
_shorten(entry["address"]),
entry["identity"],
amount,
)
console.print(contrib_table)
return True, f"Displayed info for crowdloan #{crowdloan_id}"