meshtensor-cli 9.18.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. meshtensor_cli/__init__.py +22 -0
  2. meshtensor_cli/cli.py +10742 -0
  3. meshtensor_cli/doc_generation_helper.py +4 -0
  4. meshtensor_cli/src/__init__.py +1085 -0
  5. meshtensor_cli/src/commands/__init__.py +0 -0
  6. meshtensor_cli/src/commands/axon/__init__.py +0 -0
  7. meshtensor_cli/src/commands/axon/axon.py +132 -0
  8. meshtensor_cli/src/commands/crowd/__init__.py +0 -0
  9. meshtensor_cli/src/commands/crowd/contribute.py +621 -0
  10. meshtensor_cli/src/commands/crowd/contributors.py +200 -0
  11. meshtensor_cli/src/commands/crowd/create.py +783 -0
  12. meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
  13. meshtensor_cli/src/commands/crowd/refund.py +233 -0
  14. meshtensor_cli/src/commands/crowd/update.py +418 -0
  15. meshtensor_cli/src/commands/crowd/utils.py +124 -0
  16. meshtensor_cli/src/commands/crowd/view.py +991 -0
  17. meshtensor_cli/src/commands/governance/__init__.py +0 -0
  18. meshtensor_cli/src/commands/governance/governance.py +794 -0
  19. meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
  20. meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
  21. meshtensor_cli/src/commands/liquidity/utils.py +202 -0
  22. meshtensor_cli/src/commands/proxy.py +700 -0
  23. meshtensor_cli/src/commands/stake/__init__.py +0 -0
  24. meshtensor_cli/src/commands/stake/add.py +799 -0
  25. meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
  26. meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
  27. meshtensor_cli/src/commands/stake/claim.py +770 -0
  28. meshtensor_cli/src/commands/stake/list.py +738 -0
  29. meshtensor_cli/src/commands/stake/move.py +1211 -0
  30. meshtensor_cli/src/commands/stake/remove.py +1466 -0
  31. meshtensor_cli/src/commands/stake/wizard.py +323 -0
  32. meshtensor_cli/src/commands/subnets/__init__.py +0 -0
  33. meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
  34. meshtensor_cli/src/commands/subnets/price.py +733 -0
  35. meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
  36. meshtensor_cli/src/commands/sudo.py +1294 -0
  37. meshtensor_cli/src/commands/tc/__init__.py +0 -0
  38. meshtensor_cli/src/commands/tc/tc.py +190 -0
  39. meshtensor_cli/src/commands/treasury/__init__.py +0 -0
  40. meshtensor_cli/src/commands/treasury/treasury.py +194 -0
  41. meshtensor_cli/src/commands/view.py +354 -0
  42. meshtensor_cli/src/commands/wallets.py +2311 -0
  43. meshtensor_cli/src/commands/weights.py +467 -0
  44. meshtensor_cli/src/meshtensor/__init__.py +0 -0
  45. meshtensor_cli/src/meshtensor/balances.py +313 -0
  46. meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
  47. meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
  48. meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
  49. meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
  50. meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
  51. meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
  52. meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
  53. meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
  54. meshtensor_cli/src/meshtensor/minigraph.py +254 -0
  55. meshtensor_cli/src/meshtensor/networking.py +12 -0
  56. meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
  57. meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
  58. meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
  59. meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
  60. meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
  61. meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
  62. meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
  63. meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
  64. meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
  65. meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
  66. meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
  67. meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
  68. meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
  69. meshtensor_cli/src/meshtensor/utils.py +2007 -0
  70. meshtensor_cli/version.py +23 -0
  71. meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
  72. meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
  73. meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
  74. meshtensor_cli-9.18.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,991 @@
1
+ from typing import Optional
2
+
3
+ import asyncio
4
+ import json
5
+ from meshtensor_wallet import Wallet
6
+ from rich import box
7
+ from rich.table import Column, Table
8
+
9
+ from meshtensor_cli.src import COLORS
10
+ from meshtensor_cli.src.meshtensor.balances import Balance
11
+ from meshtensor_cli.src.meshtensor.chain_data import CrowdloanData
12
+ from meshtensor_cli.src.meshtensor.meshtensor_interface import MeshtensorInterface
13
+ from meshtensor_cli.src.meshtensor.utils import (
14
+ blocks_to_duration,
15
+ console,
16
+ json_console,
17
+ print_error,
18
+ millify_tao,
19
+ )
20
+
21
+
22
+ def _shorten(account: Optional[str]) -> str:
23
+ if not account:
24
+ return "-"
25
+ return f"{account[:6]}…{account[-6:]}"
26
+
27
+
28
+ def _status(loan: CrowdloanData, current_block: int) -> str:
29
+ if loan.finalized:
30
+ return "Finalized"
31
+ if loan.raised >= loan.cap:
32
+ return "Funded"
33
+ if current_block >= loan.end:
34
+ return "Closed"
35
+ return "Active"
36
+
37
+
38
+ def _time_remaining(loan: CrowdloanData, current_block: int) -> str:
39
+ diff = loan.end - current_block
40
+ if diff > 0:
41
+ return blocks_to_duration(diff)
42
+ if diff == 0:
43
+ return "due"
44
+ return f"Closed {blocks_to_duration(abs(diff))} ago"
45
+
46
+
47
+ def _get_loan_type(loan: CrowdloanData) -> str:
48
+ """Determine if a loan is subnet leasing or fundraising."""
49
+ if loan.call_details:
50
+ pallet = loan.call_details.get("pallet", "")
51
+ method = loan.call_details.get("method", "")
52
+ if pallet == "MeshtensorModule" and method == "register_leased_network":
53
+ return "subnet"
54
+ # If has_call is True, it likely indicates a subnet loan
55
+ # (subnet loans have calls attached, fundraising loans typically don't)
56
+ if loan.has_call:
57
+ return "subnet"
58
+ # Default to fundraising if no call attached
59
+ return "fundraising"
60
+
61
+
62
+ async def list_crowdloans(
63
+ meshtensor: MeshtensorInterface,
64
+ verbose: bool = False,
65
+ json_output: bool = False,
66
+ status_filter: Optional[str] = None,
67
+ type_filter: Optional[str] = None,
68
+ sort_by: Optional[str] = None,
69
+ sort_order: Optional[str] = None,
70
+ search_creator: Optional[str] = None,
71
+ ) -> bool:
72
+ """List all crowdloans in a tabular format or JSON output.
73
+
74
+ Args:
75
+ meshtensor: MeshtensorInterface object for chain interaction
76
+ verbose: Show full addresses and precise amounts
77
+ json_output: Output as JSON
78
+ status_filter: Filter by status (active, funded, closed, finalized)
79
+ type_filter: Filter by type (subnet, fundraising)
80
+ sort_by: Sort by field (raised, end, contributors, id)
81
+ sort_order: Sort order (asc, desc)
82
+ search_creator: Search by creator address or identity name
83
+ """
84
+
85
+ current_block, loans, all_identities = await asyncio.gather(
86
+ meshtensor.substrate.get_block_number(None),
87
+ meshtensor.get_crowdloans(),
88
+ meshtensor.query_all_identities(),
89
+ )
90
+ if not loans:
91
+ if json_output:
92
+ json_console.print(
93
+ json.dumps(
94
+ {
95
+ "success": True,
96
+ "error": None,
97
+ "data": {
98
+ "crowdloans": [],
99
+ "total_count": 0,
100
+ "total_raised": 0,
101
+ "total_cap": 0,
102
+ "total_contributors": 0,
103
+ },
104
+ }
105
+ )
106
+ )
107
+ else:
108
+ console.print("[yellow]No crowdloans found.[/yellow]")
109
+ return True
110
+
111
+ # Build identity map from all identities
112
+ identity_map = {}
113
+ addresses_to_check = set()
114
+ for loan in loans.values():
115
+ addresses_to_check.add(loan.creator)
116
+ if loan.target_address:
117
+ addresses_to_check.add(loan.target_address)
118
+
119
+ for address in addresses_to_check:
120
+ identity = all_identities.get(address)
121
+ if identity:
122
+ identity_name = identity.get("name") or identity.get("display")
123
+ if identity_name:
124
+ identity_map[address] = identity_name
125
+
126
+ # Apply filters
127
+ filtered_loans = {}
128
+ for loan_id, loan in loans.items():
129
+ # Filter by status
130
+ if status_filter:
131
+ loan_status = _status(loan, current_block)
132
+ if loan_status.lower() != status_filter.lower():
133
+ continue
134
+
135
+ # Filter by type
136
+ if type_filter:
137
+ loan_type = _get_loan_type(loan)
138
+ if loan_type.lower() != type_filter.lower():
139
+ continue
140
+
141
+ # Filter by creator search
142
+ if search_creator:
143
+ search_term = search_creator.lower()
144
+ creator_match = loan.creator.lower().find(search_term) != -1
145
+ identity_match = False
146
+ if loan.creator in identity_map:
147
+ identity_name = identity_map[loan.creator].lower()
148
+ identity_match = identity_name.find(search_term) != -1
149
+ if not creator_match and not identity_match:
150
+ continue
151
+
152
+ filtered_loans[loan_id] = loan
153
+
154
+ if not filtered_loans:
155
+ if json_output:
156
+ json_console.print(
157
+ json.dumps(
158
+ {
159
+ "success": True,
160
+ "error": None,
161
+ "data": {
162
+ "crowdloans": [],
163
+ "total_count": 0,
164
+ "total_raised": 0,
165
+ "total_cap": 0,
166
+ "total_contributors": 0,
167
+ },
168
+ }
169
+ )
170
+ )
171
+ else:
172
+ console.print("[yellow]No crowdloans found matching the filters.[/yellow]")
173
+ return True
174
+
175
+ total_raised = sum(loan.raised.tao for loan in filtered_loans.values())
176
+ total_cap = sum(loan.cap.tao for loan in filtered_loans.values())
177
+ total_loans = len(filtered_loans)
178
+ total_contributors = sum(
179
+ loan.contributors_count for loan in filtered_loans.values()
180
+ )
181
+
182
+ funding_percentage = (total_raised / total_cap * 100) if total_cap > 0 else 0
183
+ percentage_color = "dark_sea_green" if funding_percentage < 100 else "red"
184
+ formatted_percentage = (
185
+ f"[{percentage_color}]{funding_percentage:.2f}%[/{percentage_color}]"
186
+ )
187
+
188
+ if json_output:
189
+ crowdloans_list = []
190
+ for loan_id, loan in filtered_loans.items():
191
+ status = _status(loan, current_block)
192
+ time_remaining = _time_remaining(loan, current_block)
193
+
194
+ call_info = None
195
+ if loan.call_details:
196
+ pallet = loan.call_details.get("pallet", "")
197
+ method = loan.call_details.get("method", "")
198
+ if pallet == "MeshtensorModule" and method == "register_leased_network":
199
+ call_info = "Subnet Leasing"
200
+ else:
201
+ call_info = (
202
+ f"{pallet}.{method}"
203
+ if pallet and method
204
+ else method or pallet or "Unknown"
205
+ )
206
+ elif loan.has_call:
207
+ call_info = "Unknown"
208
+
209
+ crowdloan_data = {
210
+ "id": loan_id,
211
+ "status": status,
212
+ "raised": loan.raised.tao,
213
+ "cap": loan.cap.tao,
214
+ "deposit": loan.deposit.tao,
215
+ "min_contribution": loan.min_contribution.tao,
216
+ "end_block": loan.end,
217
+ "time_remaining": time_remaining,
218
+ "contributors_count": loan.contributors_count,
219
+ "creator": loan.creator,
220
+ "creator_identity": identity_map.get(loan.creator),
221
+ "target_address": loan.target_address,
222
+ "target_identity": identity_map.get(loan.target_address)
223
+ if loan.target_address
224
+ else None,
225
+ "funds_account": loan.funds_account,
226
+ "call": call_info,
227
+ "finalized": loan.finalized,
228
+ }
229
+ crowdloans_list.append(crowdloan_data)
230
+
231
+ # Apply sorting
232
+ if sort_by:
233
+ reverse_order = True
234
+ if sort_order:
235
+ reverse_order = sort_order.lower() == "desc"
236
+ elif sort_by.lower() == "id":
237
+ reverse_order = False
238
+
239
+ if sort_by.lower() == "raised":
240
+ crowdloans_list.sort(key=lambda x: x["raised"], reverse=reverse_order)
241
+ elif sort_by.lower() == "end":
242
+ crowdloans_list.sort(
243
+ key=lambda x: x["end_block"], reverse=reverse_order
244
+ )
245
+ elif sort_by.lower() == "contributors":
246
+ crowdloans_list.sort(
247
+ key=lambda x: x["contributors_count"], reverse=reverse_order
248
+ )
249
+ elif sort_by.lower() == "id":
250
+ crowdloans_list.sort(key=lambda x: x["id"], reverse=reverse_order)
251
+ else:
252
+ # Default sorting: Active first, then by raised amount descending
253
+ crowdloans_list.sort(
254
+ key=lambda x: (
255
+ x["status"] != "Active",
256
+ -x["raised"],
257
+ )
258
+ )
259
+
260
+ output_dict = {
261
+ "success": True,
262
+ "error": None,
263
+ "data": {
264
+ "crowdloans": crowdloans_list,
265
+ "total_count": total_loans,
266
+ "total_raised": total_raised,
267
+ "total_cap": total_cap,
268
+ "total_contributors": total_contributors,
269
+ "funding_percentage": funding_percentage,
270
+ "current_block": current_block,
271
+ "network": meshtensor.network,
272
+ },
273
+ }
274
+ json_console.print(json.dumps(output_dict))
275
+ return True
276
+
277
+ if not verbose:
278
+ funding_string = f"τ {millify_tao(total_raised)}/{millify_tao(total_cap)} ({formatted_percentage})"
279
+ else:
280
+ funding_string = (
281
+ f"τ {total_raised:.1f}/{total_cap:.1f} ({formatted_percentage})"
282
+ )
283
+
284
+ table = Table(
285
+ title=f"\n[{COLORS.G.HEADER}]Crowdloans"
286
+ f"\nNetwork: [{COLORS.G.SUBHEAD}]{meshtensor.network}\n\n",
287
+ show_footer=True,
288
+ show_edge=False,
289
+ header_style="bold white",
290
+ border_style="bright_black",
291
+ style="bold",
292
+ title_justify="center",
293
+ show_lines=False,
294
+ pad_edge=True,
295
+ )
296
+
297
+ table.add_column(
298
+ "[bold white]ID", style="grey89", justify="center", footer=str(total_loans)
299
+ )
300
+ table.add_column("[bold white]Status", style="cyan", justify="center")
301
+ table.add_column(
302
+ f"[bold white]Raised / Cap\n({Balance.get_unit(0)})",
303
+ style="dark_sea_green2",
304
+ justify="left",
305
+ footer=funding_string,
306
+ )
307
+ table.add_column(
308
+ f"[bold white]Deposit\n({Balance.get_unit(0)})",
309
+ style="steel_blue3",
310
+ justify="left",
311
+ )
312
+ table.add_column(
313
+ f"[bold white]Min Contribution\n({Balance.get_unit(0)})",
314
+ style=COLORS.P.EMISSION,
315
+ justify="left",
316
+ )
317
+ table.add_column("[bold white]Ends (Block)", style=COLORS.S.MESH, justify="left")
318
+ table.add_column(
319
+ "[bold white]Time Remaining",
320
+ style=COLORS.S.ALPHA,
321
+ justify="left",
322
+ )
323
+ table.add_column(
324
+ "[bold white]Contributors",
325
+ style=COLORS.P.ALPHA_IN,
326
+ justify="center",
327
+ footer=str(total_contributors),
328
+ )
329
+ table.add_column(
330
+ "[bold white]Creator",
331
+ style=COLORS.G.TEMPO,
332
+ justify="left",
333
+ overflow="fold",
334
+ )
335
+ table.add_column(
336
+ "[bold white]Target",
337
+ style=COLORS.G.SUBHEAD_EX_1,
338
+ justify="center",
339
+ )
340
+ table.add_column(
341
+ "[bold white]Funds Account",
342
+ style=COLORS.G.SUBHEAD_EX_2,
343
+ justify="left",
344
+ overflow="fold",
345
+ )
346
+ table.add_column("[bold white]Call", style="grey89", justify="center")
347
+
348
+ # Apply sorting for table display
349
+ if sort_by:
350
+ reverse_order = True
351
+ if sort_order:
352
+ reverse_order = sort_order.lower() == "desc"
353
+ elif sort_by.lower() == "id":
354
+ reverse_order = False
355
+
356
+ if sort_by.lower() == "raised":
357
+ sorted_loans = sorted(
358
+ filtered_loans.items(),
359
+ key=lambda x: x[1].raised.tao,
360
+ reverse=reverse_order,
361
+ )
362
+ elif sort_by.lower() == "end":
363
+ sorted_loans = sorted(
364
+ filtered_loans.items(),
365
+ key=lambda x: x[1].end,
366
+ reverse=reverse_order,
367
+ )
368
+ elif sort_by.lower() == "contributors":
369
+ sorted_loans = sorted(
370
+ filtered_loans.items(),
371
+ key=lambda x: x[1].contributors_count,
372
+ reverse=reverse_order,
373
+ )
374
+ elif sort_by.lower() == "id":
375
+ sorted_loans = sorted(
376
+ filtered_loans.items(),
377
+ key=lambda x: x[0],
378
+ reverse=reverse_order,
379
+ )
380
+ else:
381
+ # Default sorting
382
+ sorted_loans = sorted(
383
+ filtered_loans.items(),
384
+ key=lambda x: (
385
+ _status(x[1], current_block) != "Active",
386
+ -x[1].raised.tao,
387
+ ),
388
+ )
389
+ else:
390
+ # Default sorting: Active loans first, then by raised amount (descending)
391
+ sorted_loans = sorted(
392
+ filtered_loans.items(),
393
+ key=lambda x: (
394
+ _status(x[1], current_block) != "Active", # Active loans first
395
+ -x[1].raised.tao, # Then by raised amount (descending)
396
+ ),
397
+ )
398
+
399
+ for loan_id, loan in sorted_loans:
400
+ status = _status(loan, current_block)
401
+ time_label = _time_remaining(loan, current_block)
402
+
403
+ raised_cell = (
404
+ f"τ {loan.raised.tao:,.4f} / τ {loan.cap.tao:,.4f}"
405
+ if verbose
406
+ else f"τ {millify_tao(loan.raised.tao)} / τ {millify_tao(loan.cap.tao)}"
407
+ )
408
+
409
+ deposit_cell = (
410
+ f"τ {loan.deposit.tao:,.4f}"
411
+ if verbose
412
+ else f"τ {millify_tao(loan.deposit.tao)}"
413
+ )
414
+
415
+ min_contrib_cell = (
416
+ f"τ {loan.min_contribution.tao:,.4f}"
417
+ if verbose
418
+ else f"τ {millify_tao(loan.min_contribution.tao)}"
419
+ )
420
+
421
+ status_color_map = {
422
+ "Finalized": COLORS.G.SUCCESS,
423
+ "Funded": COLORS.P.EMISSION,
424
+ "Closed": COLORS.G.SYM,
425
+ "Active": COLORS.G.HINT,
426
+ }
427
+ status_color = status_color_map.get(status, "white")
428
+ status_cell = f"[{status_color}]{status}[/{status_color}]"
429
+
430
+ if "Closed" in time_label:
431
+ time_cell = f"[{COLORS.G.SYM}]{time_label}[/{COLORS.G.SYM}]"
432
+ elif time_label == "due":
433
+ time_cell = f"[red]{time_label}[/red]"
434
+ else:
435
+ time_cell = time_label
436
+
437
+ # Format creator cell
438
+ creator_identity = identity_map.get(loan.creator)
439
+ address_display = loan.creator if verbose else _shorten(loan.creator)
440
+ creator_cell = (
441
+ f"{creator_identity} ({address_display})"
442
+ if creator_identity
443
+ else address_display
444
+ )
445
+
446
+ # Format target cell
447
+ if loan.target_address:
448
+ target_identity = identity_map.get(loan.target_address)
449
+ address_display = (
450
+ loan.target_address if verbose else _shorten(loan.target_address)
451
+ )
452
+ target_cell = (
453
+ f"{target_identity} ({address_display})"
454
+ if target_identity
455
+ else address_display
456
+ )
457
+ else:
458
+ target_cell = (
459
+ f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]"
460
+ )
461
+
462
+ funds_account_cell = (
463
+ loan.funds_account if verbose else _shorten(loan.funds_account)
464
+ )
465
+
466
+ if loan.call_details:
467
+ pallet = loan.call_details.get("pallet", "")
468
+ method = loan.call_details.get("method", "")
469
+
470
+ if pallet == "MeshtensorModule" and method == "register_leased_network":
471
+ call_label = "[magenta]Subnet Leasing[/magenta]"
472
+ else:
473
+ call_label = (
474
+ f"{pallet}.{method}"
475
+ if pallet and method
476
+ else method or pallet or "Unknown"
477
+ )
478
+
479
+ call_cell = call_label
480
+ elif loan.has_call:
481
+ call_cell = f"[{COLORS.G.SYM}]Unknown[/{COLORS.G.SYM}]"
482
+ else:
483
+ call_cell = "-"
484
+
485
+ table.add_row(
486
+ str(loan_id),
487
+ status_cell,
488
+ raised_cell,
489
+ deposit_cell,
490
+ min_contrib_cell,
491
+ str(loan.end),
492
+ time_cell,
493
+ str(loan.contributors_count),
494
+ creator_cell,
495
+ target_cell,
496
+ funds_account_cell,
497
+ call_cell,
498
+ )
499
+
500
+ console.print(table)
501
+
502
+ return True
503
+
504
+
505
+ async def show_crowdloan_details(
506
+ meshtensor: MeshtensorInterface,
507
+ crowdloan_id: int,
508
+ crowdloan: Optional[CrowdloanData] = None,
509
+ current_block: Optional[int] = None,
510
+ wallet: Optional[Wallet] = None,
511
+ verbose: bool = False,
512
+ json_output: bool = False,
513
+ show_contributors: bool = False,
514
+ ) -> tuple[bool, str]:
515
+ """Display detailed information about a specific crowdloan."""
516
+
517
+ if not crowdloan or not current_block:
518
+ current_block, crowdloan, all_identities = await asyncio.gather(
519
+ meshtensor.substrate.get_block_number(None),
520
+ meshtensor.get_single_crowdloan(crowdloan_id),
521
+ meshtensor.query_all_identities(),
522
+ )
523
+ else:
524
+ all_identities = await meshtensor.query_all_identities()
525
+
526
+ if not crowdloan:
527
+ error_msg = f"Crowdloan #{crowdloan_id} not found."
528
+ if json_output:
529
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
530
+ else:
531
+ print_error(f"[red]{error_msg}[/red]")
532
+ return False, error_msg
533
+
534
+ user_contribution = None
535
+ if wallet and wallet.coldkeypub:
536
+ user_contribution = await meshtensor.get_crowdloan_contribution(
537
+ crowdloan_id, wallet.coldkeypub.ss58_address
538
+ )
539
+
540
+ # Build identity map from all identities
541
+ identity_map = {}
542
+ addresses_to_check = [crowdloan.creator]
543
+ if crowdloan.target_address:
544
+ addresses_to_check.append(crowdloan.target_address)
545
+
546
+ for address in addresses_to_check:
547
+ identity = all_identities.get(address)
548
+ if identity:
549
+ identity_name = identity.get("name") or identity.get("display")
550
+ if identity_name:
551
+ identity_map[address] = identity_name
552
+
553
+ status = _status(crowdloan, current_block)
554
+ status_color_map = {
555
+ "Finalized": COLORS.G.SUCCESS,
556
+ "Funded": COLORS.P.EMISSION,
557
+ "Closed": COLORS.G.SYM,
558
+ "Active": COLORS.G.HINT,
559
+ }
560
+ status_color = status_color_map.get(status, "white")
561
+
562
+ if json_output:
563
+ time_remaining = _time_remaining(crowdloan, current_block)
564
+
565
+ avg_contribution = None
566
+ if crowdloan.contributors_count > 0:
567
+ net_contributions = crowdloan.raised.tao - crowdloan.deposit.tao
568
+ avg_contribution = (
569
+ net_contributions / (crowdloan.contributors_count - 1)
570
+ if crowdloan.contributors_count > 1
571
+ else crowdloan.deposit.tao
572
+ )
573
+
574
+ call_info = None
575
+ if crowdloan.has_call and crowdloan.call_details:
576
+ pallet = crowdloan.call_details.get("pallet", "Unknown")
577
+ method = crowdloan.call_details.get("method", "Unknown")
578
+ args = crowdloan.call_details.get("args", {})
579
+
580
+ if pallet == "MeshtensorModule" and method == "register_leased_network":
581
+ call_info = {
582
+ "type": "Subnet Leasing",
583
+ "pallet": pallet,
584
+ "method": method,
585
+ "emissions_share": args.get("emissions_share", {}).get("value"),
586
+ "end_block": args.get("end_block", {}).get("value"),
587
+ }
588
+ else:
589
+ call_info = {"pallet": pallet, "method": method, "args": args}
590
+
591
+ user_contribution_info = None
592
+ if user_contribution:
593
+ is_creator = (
594
+ wallet
595
+ and wallet.coldkeypub
596
+ and wallet.coldkeypub.ss58_address == crowdloan.creator
597
+ )
598
+ withdrawable_amount = None
599
+
600
+ if status == "Active" and not crowdloan.finalized:
601
+ if is_creator and user_contribution.tao > crowdloan.deposit.tao:
602
+ withdrawable_amount = user_contribution.tao - crowdloan.deposit.tao
603
+ elif not is_creator:
604
+ withdrawable_amount = user_contribution.tao
605
+
606
+ user_contribution_info = {
607
+ "amount": user_contribution.tao,
608
+ "is_creator": is_creator,
609
+ "withdrawable": withdrawable_amount,
610
+ "refundable": status == "Closed",
611
+ }
612
+
613
+ output_dict = {
614
+ "success": True,
615
+ "error": None,
616
+ "data": {
617
+ "crowdloan_id": crowdloan_id,
618
+ "status": status,
619
+ "finalized": crowdloan.finalized,
620
+ "creator": crowdloan.creator,
621
+ "creator_identity": identity_map.get(crowdloan.creator),
622
+ "funds_account": crowdloan.funds_account,
623
+ "raised": crowdloan.raised.tao,
624
+ "cap": crowdloan.cap.tao,
625
+ "raised_percentage": (crowdloan.raised.tao / crowdloan.cap.tao * 100)
626
+ if crowdloan.cap.tao > 0
627
+ else 0,
628
+ "deposit": crowdloan.deposit.tao,
629
+ "min_contribution": crowdloan.min_contribution.tao,
630
+ "end_block": crowdloan.end,
631
+ "current_block": current_block,
632
+ "time_remaining": time_remaining,
633
+ "contributors_count": crowdloan.contributors_count,
634
+ "average_contribution": avg_contribution,
635
+ "target_address": crowdloan.target_address,
636
+ "target_identity": identity_map.get(crowdloan.target_address)
637
+ if crowdloan.target_address
638
+ else None,
639
+ "has_call": crowdloan.has_call,
640
+ "call_details": call_info,
641
+ "user_contribution": user_contribution_info,
642
+ "network": meshtensor.network,
643
+ },
644
+ }
645
+
646
+ # Add contributors list if requested
647
+ if show_contributors:
648
+ contributor_contributions = await meshtensor.get_crowdloan_contributors(
649
+ crowdloan_id
650
+ )
651
+ contributors_list = list(contributor_contributions.keys())
652
+ if contributors_list:
653
+ contributors_json = []
654
+ total_contributed = Balance.from_tao(0)
655
+ for (
656
+ contributor_address,
657
+ contribution_amount,
658
+ ) in contributor_contributions.items():
659
+ total_contributed += contribution_amount
660
+
661
+ contributor_data = []
662
+ for contributor_address in contributors_list:
663
+ contribution_amount = contributor_contributions[contributor_address]
664
+ identity = all_identities.get(contributor_address)
665
+ identity_name = None
666
+ if identity:
667
+ identity_name = identity.get("name") or identity.get("display")
668
+ contributor_data.append(
669
+ {
670
+ "address": contributor_address,
671
+ "identity": identity_name,
672
+ "contribution": contribution_amount,
673
+ }
674
+ )
675
+
676
+ contributor_data.sort(key=lambda x: x["contribution"].meshlet, reverse=True)
677
+
678
+ for rank, data in enumerate(contributor_data, start=1):
679
+ percentage = (
680
+ (data["contribution"].meshlet / total_contributed.meshlet * 100)
681
+ if total_contributed.meshlet > 0
682
+ else 0
683
+ )
684
+ contributors_json.append(
685
+ {
686
+ "rank": rank,
687
+ "address": data["address"],
688
+ "identity": data["identity"],
689
+ "contribution_tao": data["contribution"].tao,
690
+ "contribution_meshlet": data["contribution"].meshlet,
691
+ "percentage": percentage,
692
+ }
693
+ )
694
+
695
+ output_dict["data"]["contributors"] = contributors_json
696
+
697
+ json_console.print(json.dumps(output_dict))
698
+ return True, f"Displayed info for crowdloan #{crowdloan_id}"
699
+
700
+ table = Table(
701
+ Column(
702
+ "Field",
703
+ style=COLORS.G.SUBHEAD,
704
+ min_width=20,
705
+ no_wrap=True,
706
+ ),
707
+ Column("Value", style=COLORS.G.TEMPO),
708
+ title=f"\n[underline][{COLORS.G.HEADER}]CROWDLOAN #{crowdloan_id}[/underline][/{COLORS.G.HEADER}] - [{status_color} underline]{status.upper()}[/{status_color} underline]",
709
+ show_header=False,
710
+ show_footer=False,
711
+ width=None,
712
+ pad_edge=False,
713
+ box=box.SIMPLE,
714
+ show_edge=True,
715
+ border_style="bright_black",
716
+ expand=False,
717
+ )
718
+
719
+ # OVERVIEW Section
720
+ table.add_row("[cyan underline]OVERVIEW[/cyan underline]", "")
721
+ table.add_section()
722
+
723
+ status_detail = ""
724
+ if status == "Active":
725
+ status_detail = " [dim](accepting contributions)[/dim]"
726
+ elif status == "Funded":
727
+ status_detail = " [yellow](awaiting finalization)[/yellow]"
728
+ elif status == "Closed":
729
+ status_detail = " [dim](failed to reach cap)[/dim]"
730
+ elif status == "Finalized":
731
+ status_detail = " [green](successfully completed)[/green]"
732
+
733
+ table.add_row("Status", f"[{status_color}]{status}[/{status_color}]{status_detail}")
734
+
735
+ # Display creator
736
+ creator_identity = identity_map.get(crowdloan.creator)
737
+ address_display = crowdloan.creator if verbose else _shorten(crowdloan.creator)
738
+ creator_display = (
739
+ f"{creator_identity} ({address_display})"
740
+ if creator_identity
741
+ else address_display
742
+ )
743
+ table.add_row(
744
+ "Creator",
745
+ f"[{COLORS.G.TEMPO}]{creator_display}[/{COLORS.G.TEMPO}]",
746
+ )
747
+ table.add_row(
748
+ "Funds Account",
749
+ f"[{COLORS.G.SUBHEAD_EX_2}]{crowdloan.funds_account}[/{COLORS.G.SUBHEAD_EX_2}]",
750
+ )
751
+
752
+ # FUNDING PROGRESS Section
753
+ table.add_section()
754
+ table.add_row("[cyan underline]FUNDING PROGRESS[/cyan underline]", "")
755
+ table.add_section()
756
+
757
+ raised_pct = (
758
+ (crowdloan.raised.tao / crowdloan.cap.tao * 100) if crowdloan.cap.tao > 0 else 0
759
+ )
760
+ progress_filled = int(raised_pct / 100 * 16)
761
+ progress_empty = 16 - progress_filled
762
+ progress_bar = f"[dark_sea_green]{'█' * progress_filled}[/dark_sea_green][grey35]{'░' * progress_empty}[/grey35]"
763
+
764
+ if verbose:
765
+ raised_str = f"τ {crowdloan.raised.tao:,.4f} / τ {crowdloan.cap.tao:,.4f}"
766
+ deposit_str = f"τ {crowdloan.deposit.tao:,.4f}"
767
+ min_contrib_str = f"τ {crowdloan.min_contribution.tao:,.4f}"
768
+ else:
769
+ raised_str = f"τ {millify_tao(crowdloan.raised.tao)} / τ {millify_tao(crowdloan.cap.tao)}"
770
+ deposit_str = f"τ {millify_tao(crowdloan.deposit.tao)}"
771
+ min_contrib_str = f"τ {millify_tao(crowdloan.min_contribution.tao)}"
772
+
773
+ table.add_row("Raised/Cap", raised_str)
774
+ table.add_row(
775
+ "Progress", f"{progress_bar} [dark_sea_green]{raised_pct:.2f}%[/dark_sea_green]"
776
+ )
777
+ table.add_row("Deposit", deposit_str)
778
+ table.add_row("Min Contribution", min_contrib_str)
779
+
780
+ # TIMELINE Section
781
+ table.add_section()
782
+ table.add_row("[cyan underline]TIMELINE[/cyan underline]", "")
783
+ table.add_section()
784
+
785
+ time_label = _time_remaining(crowdloan, current_block)
786
+ if "Closed" in time_label:
787
+ time_display = f"[{COLORS.G.SYM}]{time_label}[/{COLORS.G.SYM}]"
788
+ elif time_label == "due":
789
+ time_display = "[red]Due now[/red]"
790
+ else:
791
+ time_display = f"[{COLORS.S.ALPHA}]{time_label}[/{COLORS.S.ALPHA}]"
792
+
793
+ table.add_row("Ends at Block", f"{crowdloan.end}")
794
+ table.add_row("Current Block", f"{current_block}")
795
+ table.add_row("Time Remaining", time_display)
796
+
797
+ # PARTICIPATION Section
798
+ table.add_section()
799
+ table.add_row("[cyan underline]PARTICIPATION[/cyan underline]", "")
800
+ table.add_section()
801
+
802
+ table.add_row("Contributors", f"{crowdloan.contributors_count}")
803
+
804
+ if crowdloan.contributors_count > 0:
805
+ net_contributions = crowdloan.raised.tao - crowdloan.deposit.tao
806
+ avg_contribution = (
807
+ net_contributions / (crowdloan.contributors_count - 1)
808
+ if crowdloan.contributors_count > 1
809
+ else crowdloan.deposit.tao
810
+ )
811
+ if verbose:
812
+ avg_contrib_str = f"τ {avg_contribution:,.4f}"
813
+ else:
814
+ avg_contrib_str = f"τ {millify_tao(avg_contribution)}"
815
+ table.add_row("Avg Contribution", avg_contrib_str)
816
+
817
+ if user_contribution:
818
+ is_creator = wallet.coldkeypub.ss58_address == crowdloan.creator
819
+ if verbose:
820
+ user_contrib_str = f"τ {user_contribution.tao:,.4f}"
821
+ else:
822
+ user_contrib_str = f"τ {millify_tao(user_contribution.tao)}"
823
+
824
+ contrib_status = ""
825
+ if status == "Active" and not crowdloan.finalized:
826
+ if is_creator and user_contribution.tao > crowdloan.deposit.tao:
827
+ withdrawable = user_contribution.tao - crowdloan.deposit.tao
828
+ if verbose:
829
+ withdrawable_str = f"{withdrawable:,.4f}"
830
+ else:
831
+ withdrawable_str = f"{millify_tao(withdrawable)}"
832
+ contrib_status = (
833
+ f" [yellow](τ {withdrawable_str} withdrawable)[/yellow]"
834
+ )
835
+ elif not is_creator:
836
+ contrib_status = " [yellow](withdrawable)[/yellow]"
837
+ elif status == "Closed":
838
+ contrib_status = " [green](refundable)[/green]"
839
+
840
+ your_contrib_value = f"{user_contrib_str}{contrib_status}"
841
+ if is_creator:
842
+ your_contrib_value += " [dim](You are the creator)[/dim]"
843
+ table.add_row("Your Contribution", your_contrib_value)
844
+
845
+ # TARGET Section
846
+ table.add_section()
847
+ table.add_row("[cyan underline]TARGET[/cyan underline]", "")
848
+ table.add_section()
849
+
850
+ if crowdloan.target_address:
851
+ target_identity = identity_map.get(crowdloan.target_address)
852
+ address_display = (
853
+ crowdloan.target_address if verbose else _shorten(crowdloan.target_address)
854
+ )
855
+ target_display = (
856
+ f"{target_identity} ({address_display})"
857
+ if target_identity
858
+ else address_display
859
+ )
860
+ else:
861
+ target_display = (
862
+ f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]"
863
+ )
864
+
865
+ table.add_row("Address", target_display)
866
+
867
+ table.add_section()
868
+ table.add_row("[cyan underline]CALL DETAILS[/cyan underline]", "")
869
+ table.add_section()
870
+
871
+ has_call_display = (
872
+ f"[{COLORS.G.SUCCESS}]Yes[/{COLORS.G.SUCCESS}]"
873
+ if crowdloan.has_call
874
+ else f"[{COLORS.G.SYM}]No[/{COLORS.G.SYM}]"
875
+ )
876
+ table.add_row("Has Call", has_call_display)
877
+
878
+ if crowdloan.has_call and crowdloan.call_details:
879
+ pallet = crowdloan.call_details.get("pallet", "Unknown")
880
+ method = crowdloan.call_details.get("method", "Unknown")
881
+ args = crowdloan.call_details.get("args", {})
882
+
883
+ if pallet == "MeshtensorModule" and method == "register_leased_network":
884
+ table.add_row("Type", "[magenta]Subnet Leasing[/magenta]")
885
+ emissions_share = args.get("emissions_share", {}).get("value")
886
+ if emissions_share is not None:
887
+ table.add_row("Emissions Share", f"[cyan]{emissions_share}%[/cyan]")
888
+
889
+ end_block = args.get("end_block", {}).get("value")
890
+ if end_block:
891
+ table.add_row("Lease Ends", f"Block {end_block}")
892
+ else:
893
+ table.add_row("Lease Duration", "[green]Perpetual[/green]")
894
+ else:
895
+ table.add_row("Pallet", pallet)
896
+ table.add_row("Method", method)
897
+ if args:
898
+ for arg_name, arg_data in args.items():
899
+ if isinstance(arg_data, dict):
900
+ display_value = arg_data.get("value")
901
+ arg_type = arg_data.get("type")
902
+ else:
903
+ display_value = arg_data
904
+ arg_type = None
905
+
906
+ if arg_type:
907
+ table.add_row(
908
+ f"{arg_name} [{arg_type}]",
909
+ str(display_value),
910
+ )
911
+ else:
912
+ table.add_row(arg_name, str(display_value))
913
+
914
+ # CONTRIBUTORS Section (if requested)
915
+ if show_contributors:
916
+ table.add_section()
917
+ table.add_row("[cyan underline]CONTRIBUTORS[/cyan underline]", "")
918
+ table.add_section()
919
+
920
+ # Fetch contributors
921
+ contributor_contributions = await meshtensor.get_crowdloan_contributors(
922
+ crowdloan_id
923
+ )
924
+
925
+ if contributor_contributions:
926
+ contributors_list = list(contributor_contributions.keys())
927
+ contributor_data = []
928
+ total_contributed = Balance.from_tao(0)
929
+
930
+ for contributor_address in contributors_list:
931
+ contribution_amount = contributor_contributions[contributor_address]
932
+ total_contributed += contribution_amount
933
+ identity = all_identities.get(contributor_address)
934
+ identity_name = None
935
+ if identity:
936
+ identity_name = identity.get("name") or identity.get("display")
937
+
938
+ contributor_data.append(
939
+ {
940
+ "address": contributor_address,
941
+ "identity": identity_name,
942
+ "contribution": contribution_amount,
943
+ }
944
+ )
945
+
946
+ # Sort by contribution amount (descending)
947
+ contributor_data.sort(key=lambda x: x["contribution"].meshlet, reverse=True)
948
+
949
+ # Display contributors in table
950
+ for rank, data in enumerate(contributor_data[:10], start=1): # Show top 10
951
+ address_display = (
952
+ data["address"] if verbose else _shorten(data["address"])
953
+ )
954
+ identity_display = (
955
+ data["identity"] if data["identity"] else "[dim]-[/dim]"
956
+ )
957
+
958
+ if data["identity"]:
959
+ if verbose:
960
+ contributor_display = f"{identity_display} ({address_display})"
961
+ else:
962
+ contributor_display = f"{identity_display} ({address_display})"
963
+ else:
964
+ contributor_display = address_display
965
+
966
+ if verbose:
967
+ contribution_display = f"τ {data['contribution'].tao:,.4f}"
968
+ else:
969
+ contribution_display = f"τ {millify_tao(data['contribution'].tao)}"
970
+
971
+ percentage = (
972
+ (data["contribution"].meshlet / total_contributed.meshlet * 100)
973
+ if total_contributed.meshlet > 0
974
+ else 0
975
+ )
976
+
977
+ table.add_row(
978
+ f"#{rank}",
979
+ f"{contributor_display:<70} - {contribution_display} ({percentage:.2f}%)",
980
+ )
981
+
982
+ if len(contributor_data) > 10:
983
+ table.add_row(
984
+ "",
985
+ f"[dim]... and {len(contributor_data) - 10} more contributors[/dim]",
986
+ )
987
+ else:
988
+ table.add_row("", "[dim]No contributors yet[/dim]")
989
+
990
+ console.print(table)
991
+ return True, f"Displayed info for crowdloan #{crowdloan_id}"