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,783 @@
1
+ import asyncio
2
+ import json
3
+ from typing import Optional
4
+
5
+ from meshtensor_wallet import Wallet
6
+ from rich.prompt import IntPrompt, Prompt, FloatPrompt
7
+ from rich.table import Table, Column, box
8
+ from scalecodec import GenericCall
9
+ from meshtensor_cli.src import COLORS
10
+ from meshtensor_cli.src.commands.crowd.view import show_crowdloan_details
11
+ from meshtensor_cli.src.meshtensor.balances import Balance
12
+ from meshtensor_cli.src.meshtensor.meshtensor_interface import MeshtensorInterface
13
+ from meshtensor_cli.src.commands.crowd.utils import (
14
+ get_constant,
15
+ prompt_custom_call_params,
16
+ )
17
+ from meshtensor_cli.src.meshtensor.utils import (
18
+ blocks_to_duration,
19
+ confirm_action,
20
+ console,
21
+ json_console,
22
+ print_error,
23
+ print_success,
24
+ is_valid_ss58_address,
25
+ unlock_key,
26
+ print_extrinsic_id,
27
+ )
28
+
29
+
30
+ async def create_crowdloan(
31
+ meshtensor: MeshtensorInterface,
32
+ wallet: Wallet,
33
+ proxy: Optional[str],
34
+ deposit_tao: Optional[int],
35
+ min_contribution_tao: Optional[int],
36
+ cap_tao: Optional[int],
37
+ duration_blocks: Optional[int],
38
+ target_address: Optional[str],
39
+ subnet_lease: Optional[bool],
40
+ emissions_share: Optional[int],
41
+ lease_end_block: Optional[int],
42
+ custom_call_pallet: Optional[str],
43
+ custom_call_method: Optional[str],
44
+ custom_call_args: Optional[str],
45
+ wait_for_inclusion: bool,
46
+ wait_for_finalization: bool,
47
+ prompt: bool,
48
+ decline: bool = False,
49
+ quiet: bool = False,
50
+ json_output: bool = False,
51
+ ) -> tuple[bool, str]:
52
+ """
53
+ Create a new crowdloan with the given parameters.
54
+ Prompts for missing parameters if not provided.
55
+ """
56
+
57
+ unlock_status = unlock_key(wallet)
58
+ if not unlock_status.success:
59
+ if json_output:
60
+ json_console.print(
61
+ json.dumps({"success": False, "error": unlock_status.message})
62
+ )
63
+ else:
64
+ print_error(f"[red]{unlock_status.message}[/red]")
65
+ return False, unlock_status.message
66
+
67
+ # Determine crowdloan type and validate
68
+ crowdloan_type: str
69
+ if subnet_lease is not None:
70
+ if custom_call_pallet or custom_call_method or custom_call_args:
71
+ error_msg = "--custom-call-* cannot be used with --subnet-lease."
72
+ if json_output:
73
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
74
+ else:
75
+ print_error(f"[red]{error_msg}[/red]")
76
+ return False, error_msg
77
+ crowdloan_type = "subnet" if subnet_lease else "fundraising"
78
+ elif custom_call_pallet or custom_call_method or custom_call_args:
79
+ if not (custom_call_pallet and custom_call_method):
80
+ error_msg = (
81
+ "Both --custom-call-pallet and --custom-call-method must be provided."
82
+ )
83
+ if json_output:
84
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
85
+ else:
86
+ print_error(f"[red]{error_msg}[/red]")
87
+ return False, error_msg
88
+ crowdloan_type = "custom"
89
+ elif prompt:
90
+ type_choice = IntPrompt.ask(
91
+ "\n[bold cyan]What type of crowdloan would you like to create?[/bold cyan]\n"
92
+ "[cyan][1][/cyan] General Fundraising (funds go to address)\n"
93
+ "[cyan][2][/cyan] Subnet Leasing (create new subnet)\n"
94
+ "[cyan][3][/cyan] Custom Call (attach custom Substrate call)",
95
+ choices=["1", "2", "3"],
96
+ )
97
+
98
+ if type_choice == 2:
99
+ crowdloan_type = "subnet"
100
+ elif type_choice == 3:
101
+ crowdloan_type = "custom"
102
+ success, pallet, method, args, error_msg = await prompt_custom_call_params(
103
+ meshtensor=meshtensor, json_output=json_output
104
+ )
105
+ if not success:
106
+ return False, error_msg or "Failed to get custom call parameters."
107
+ custom_call_pallet, custom_call_method, custom_call_args = (
108
+ pallet,
109
+ method,
110
+ args,
111
+ )
112
+ else:
113
+ crowdloan_type = "fundraising"
114
+
115
+ if crowdloan_type == "subnet":
116
+ current_burn_cost = await meshtensor.burn_cost()
117
+ console.print(
118
+ "\n[magenta]Subnet Lease Crowdloan Selected[/magenta]\n"
119
+ " • A new subnet will be created when the crowdloan is finalized\n"
120
+ " • Contributors will receive emissions as dividends\n"
121
+ " • You will become the subnet operator\n"
122
+ f" • [yellow]Note: Ensure cap covers subnet registration cost (currently {current_burn_cost.tao:,.2f} MESH)[/yellow]\n"
123
+ )
124
+ elif crowdloan_type == "custom":
125
+ console.print(
126
+ "\n[yellow]Custom Call Crowdloan Selected[/yellow]\n"
127
+ " • A custom Substrate call will be executed when the crowdloan is finalized\n"
128
+ " • Ensure the call parameters are correct before proceeding\n"
129
+ )
130
+ else:
131
+ console.print(
132
+ "\n[cyan]General Fundraising Crowdloan Selected[/cyan]\n"
133
+ " • Funds will be transferred to a target address when finalized\n"
134
+ " • Contributors can withdraw if the cap is not reached\n"
135
+ )
136
+ else:
137
+ error_msg = "Crowdloan type not specified and no prompt provided."
138
+ if json_output:
139
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
140
+ else:
141
+ print_error(error_msg)
142
+ return False, error_msg
143
+
144
+ block_hash = await meshtensor.substrate.get_chain_head()
145
+ runtime = await meshtensor.substrate.init_runtime(block_hash=block_hash)
146
+ (
147
+ minimum_deposit_raw,
148
+ min_contribution_raw,
149
+ min_duration,
150
+ max_duration,
151
+ ) = await asyncio.gather(
152
+ get_constant(meshtensor, "MinimumDeposit", runtime=runtime),
153
+ get_constant(meshtensor, "AbsoluteMinimumContribution", runtime=runtime),
154
+ get_constant(meshtensor, "MinimumBlockDuration", runtime=runtime),
155
+ get_constant(meshtensor, "MaximumBlockDuration", runtime=runtime),
156
+ )
157
+
158
+ minimum_deposit = Balance.from_meshlet(minimum_deposit_raw)
159
+ min_contribution = Balance.from_meshlet(min_contribution_raw)
160
+
161
+ if not prompt:
162
+ missing_fields = []
163
+ if deposit_tao is None:
164
+ missing_fields.append("--deposit")
165
+ if min_contribution_tao is None:
166
+ missing_fields.append("--min-contribution")
167
+ if cap_tao is None:
168
+ missing_fields.append("--cap")
169
+ if duration_blocks is None:
170
+ missing_fields.append("--duration")
171
+ if missing_fields:
172
+ error_msg = (
173
+ "The following options must be provided when prompts are disabled: "
174
+ + ", ".join(missing_fields)
175
+ )
176
+ if json_output:
177
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
178
+ else:
179
+ print_error(f"[red]{error_msg}[/red]")
180
+ return False, "Missing required options when prompts are disabled."
181
+ duration = 0
182
+ deposit_value = deposit_tao
183
+ while True:
184
+ if deposit_value is None:
185
+ deposit_value = FloatPrompt.ask(
186
+ f"Enter the deposit amount in MESH "
187
+ f"[blue](>= {minimum_deposit.tao:,.4f})[/blue]"
188
+ )
189
+ deposit = Balance.from_tao(deposit_value)
190
+ if deposit < minimum_deposit:
191
+ if prompt:
192
+ print_error(
193
+ f"[red]Deposit must be at least {minimum_deposit.tao:,.4f} MESH.[/red]"
194
+ )
195
+ deposit_value = None
196
+ continue
197
+ error_msg = f"Deposit is below the minimum required deposit ({minimum_deposit.tao} MESH)."
198
+ if json_output:
199
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
200
+ else:
201
+ print_error(f"[red]{error_msg}[/red]")
202
+ return False, "Deposit is below the minimum required deposit."
203
+ break
204
+
205
+ min_contribution_value = min_contribution_tao
206
+ while True:
207
+ if min_contribution_value is None:
208
+ min_contribution_value = FloatPrompt.ask(
209
+ f"Enter the minimum contribution amount in MESH "
210
+ f"[blue](>= {min_contribution.tao:,.4f})[/blue]"
211
+ )
212
+ min_contribution = Balance.from_tao(min_contribution_value)
213
+ if min_contribution < min_contribution:
214
+ if prompt:
215
+ print_error(
216
+ f"[red]Minimum contribution must be at least "
217
+ f"{min_contribution.tao:,.4f} MESH.[/red]"
218
+ )
219
+ min_contribution_value = None
220
+ continue
221
+ print_error(
222
+ "[red]Minimum contribution is below the chain's absolute minimum.[/red]"
223
+ )
224
+ return False, "Minimum contribution is below the chain's absolute minimum."
225
+ break
226
+
227
+ cap_value = cap_tao
228
+ while True:
229
+ if cap_value is None:
230
+ cap_value = FloatPrompt.ask(
231
+ f"Enter the cap amount in MESH [blue](> deposit of {deposit.tao:,.4f})[/blue]"
232
+ )
233
+ cap = Balance.from_tao(cap_value)
234
+ if cap <= deposit:
235
+ if prompt:
236
+ print_error(
237
+ f"Cap must be greater than the deposit ({deposit.tao:,.4f} MESH)."
238
+ )
239
+ cap_value = None
240
+ continue
241
+ print_error("Cap must be greater than the initial deposit.")
242
+ return False, "Cap must be greater than the initial deposit."
243
+ break
244
+
245
+ duration_value = duration_blocks
246
+ while True:
247
+ if duration_value is None:
248
+ duration_value = IntPrompt.ask(
249
+ f"Enter the crowdloan duration in blocks "
250
+ f"[blue]({min_duration} - {max_duration})[/blue]"
251
+ )
252
+ if duration_value < min_duration or duration_value > max_duration:
253
+ if prompt:
254
+ print_error(
255
+ f"Duration must be between {min_duration} and "
256
+ f"{max_duration} blocks."
257
+ )
258
+ duration_value = None
259
+ continue
260
+ print_error("Crowdloan duration is outside the allowed range.")
261
+ return False, "Crowdloan duration is outside the allowed range."
262
+ duration = duration_value
263
+ break
264
+
265
+ current_block = await meshtensor.substrate.get_block_number(None)
266
+ call_to_attach: Optional[GenericCall]
267
+ lease_perpetual = None
268
+ custom_call_info: Optional[dict] = None
269
+
270
+ if crowdloan_type == "custom":
271
+ call_params = json.loads(custom_call_args or "{}")
272
+ call_to_attach, error_msg = await meshtensor.compose_custom_crowdloan_call(
273
+ pallet_name=custom_call_pallet,
274
+ method_name=custom_call_method,
275
+ call_params=call_params,
276
+ )
277
+
278
+ if call_to_attach is None:
279
+ if json_output:
280
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
281
+ else:
282
+ print_error(f"[red]{error_msg}[/red]")
283
+ return False, error_msg or "Failed to compose custom call."
284
+
285
+ custom_call_info = {
286
+ "pallet": custom_call_pallet,
287
+ "method": custom_call_method,
288
+ "args": call_params,
289
+ }
290
+ target_address = None # Custom calls don't use target_address
291
+ elif crowdloan_type == "subnet":
292
+ target_address = None
293
+
294
+ if emissions_share is None:
295
+ emissions_share = IntPrompt.ask(
296
+ "Enter emissions share percentage for contributors [blue](0-100)[/blue]"
297
+ )
298
+
299
+ if not 0 <= emissions_share <= 100:
300
+ print_error(
301
+ f"Emissions share must be between 0 and 100, got {emissions_share}"
302
+ )
303
+ return False, "Invalid emissions share percentage."
304
+
305
+ if lease_end_block is None:
306
+ lease_perpetual = confirm_action(
307
+ "Should the subnet lease be perpetual?",
308
+ default=True,
309
+ decline=decline,
310
+ quiet=quiet,
311
+ )
312
+ if not lease_perpetual:
313
+ lease_end_block = IntPrompt.ask(
314
+ f"Enter the block number when the lease should end. Current block is [bold]{current_block}[/bold]."
315
+ )
316
+ register_lease_call = await meshtensor.substrate.compose_call(
317
+ call_module="MeshtensorModule",
318
+ call_function="register_leased_network",
319
+ call_params={
320
+ "emissions_share": emissions_share,
321
+ "end_block": None if lease_perpetual else lease_end_block,
322
+ },
323
+ )
324
+ call_to_attach = register_lease_call
325
+ else:
326
+ if target_address:
327
+ target_address = target_address.strip()
328
+ if not is_valid_ss58_address(target_address):
329
+ print_error(f"Invalid target SS58 address provided: {target_address}")
330
+ return False, "Invalid target SS58 address provided."
331
+ elif prompt:
332
+ target_input = Prompt.ask(
333
+ "Enter a target SS58 address",
334
+ )
335
+ target_address = target_input.strip() or None
336
+
337
+ if not is_valid_ss58_address(target_address):
338
+ print_error(f"Invalid target SS58 address provided: {target_address}")
339
+ return False, "Invalid target SS58 address provided."
340
+
341
+ call_to_attach = None
342
+
343
+ creator_balance = await meshtensor.get_balance(
344
+ proxy or wallet.coldkeypub.ss58_address
345
+ )
346
+ if deposit > creator_balance:
347
+ print_error(
348
+ f"Insufficient balance to cover the deposit. "
349
+ f"Available: {creator_balance}, required: {deposit}"
350
+ )
351
+ return False, "Insufficient balance to cover the deposit."
352
+
353
+ end_block = current_block + duration
354
+
355
+ call = await meshtensor.substrate.compose_call(
356
+ call_module="Crowdloan",
357
+ call_function="create",
358
+ call_params={
359
+ "deposit": deposit.meshlet,
360
+ "min_contribution": min_contribution.meshlet,
361
+ "cap": cap.meshlet,
362
+ "end": end_block,
363
+ "call": call_to_attach,
364
+ "target_address": target_address,
365
+ },
366
+ )
367
+
368
+ extrinsic_fee = await meshtensor.get_extrinsic_fee(
369
+ call, wallet.coldkeypub, proxy=proxy
370
+ )
371
+
372
+ if prompt:
373
+ duration_text = blocks_to_duration(duration)
374
+
375
+ table = Table(
376
+ Column("[bold white]Field", style=COLORS.G.SUBHEAD),
377
+ Column("[bold white]Value", style=COLORS.G.TEMPO),
378
+ title=f"\n[bold cyan]Crowdloan Creation Summary[/bold cyan]\n"
379
+ f"Network: [{COLORS.G.SUBHEAD_MAIN}]{meshtensor.network}[/{COLORS.G.SUBHEAD_MAIN}]",
380
+ show_footer=False,
381
+ show_header=False,
382
+ width=None,
383
+ pad_edge=False,
384
+ box=box.SIMPLE,
385
+ show_edge=True,
386
+ border_style="bright_black",
387
+ )
388
+
389
+ if crowdloan_type == "subnet":
390
+ table.add_row("Type", "[magenta]Subnet Leasing[/magenta]")
391
+ table.add_row(
392
+ "Emissions Share", f"[cyan]{emissions_share}%[/cyan] for contributors"
393
+ )
394
+ if lease_end_block:
395
+ table.add_row("Lease Ends", f"Block {lease_end_block}")
396
+ else:
397
+ table.add_row("Lease Duration", "[green]Perpetual[/green]")
398
+ elif crowdloan_type == "custom":
399
+ table.add_row("Type", "[yellow]Custom Call[/yellow]")
400
+ table.add_row("Pallet", f"[cyan]{custom_call_info['pallet']}[/cyan]")
401
+ table.add_row("Method", f"[cyan]{custom_call_info['method']}[/cyan]")
402
+ args_str = (
403
+ json.dumps(custom_call_info["args"], indent=2)
404
+ if custom_call_info["args"]
405
+ else "{}"
406
+ )
407
+ table.add_row("Call Arguments", f"[dim]{args_str}[/dim]")
408
+ else:
409
+ table.add_row("Type", "[cyan]General Fundraising[/cyan]")
410
+ target_text = (
411
+ target_address
412
+ if target_address
413
+ else f"[{COLORS.G.SUBHEAD_MAIN}]Not specified[/{COLORS.G.SUBHEAD_MAIN}]"
414
+ )
415
+ table.add_row("Target address", target_text)
416
+
417
+ table.add_row("Deposit", f"[{COLORS.P.MESH}]{deposit}[/{COLORS.P.MESH}]")
418
+ table.add_row(
419
+ "Min contribution", f"[{COLORS.P.MESH}]{min_contribution}[/{COLORS.P.MESH}]"
420
+ )
421
+ table.add_row("Cap", f"[{COLORS.P.MESH}]{cap}[/{COLORS.P.MESH}]")
422
+ table.add_row("Duration", f"[bold]{duration}[/bold] blocks (~{duration_text})")
423
+ table.add_row("Ends at block", f"[bold]{end_block}[/bold]")
424
+ table.add_row(
425
+ "Estimated fee",
426
+ f"[{COLORS.P.MESH}]{extrinsic_fee}[/{COLORS.P.MESH}]"
427
+ + (" (paid by signer account)" if proxy else ""),
428
+ )
429
+ console.print(table)
430
+
431
+ if not confirm_action(
432
+ "Proceed with creating the crowdloan?", decline=decline, quiet=quiet
433
+ ):
434
+ if json_output:
435
+ json_console.print(
436
+ json.dumps(
437
+ {"success": False, "error": "Cancelled crowdloan creation."}
438
+ )
439
+ )
440
+ else:
441
+ console.print("[yellow]Cancelled crowdloan creation.[/yellow]")
442
+ return False, "Cancelled crowdloan creation."
443
+
444
+ success, error_message, extrinsic_receipt = await meshtensor.sign_and_send_extrinsic(
445
+ call=call,
446
+ wallet=wallet,
447
+ proxy=proxy,
448
+ wait_for_inclusion=wait_for_inclusion,
449
+ wait_for_finalization=wait_for_finalization,
450
+ )
451
+
452
+ if not success:
453
+ if json_output:
454
+ json_console.print(
455
+ json.dumps(
456
+ {
457
+ "success": False,
458
+ "error": error_message or "Failed to create crowdloan.",
459
+ }
460
+ )
461
+ )
462
+ else:
463
+ print_error(f"{error_message or 'Failed to create crowdloan.'}")
464
+ return False, error_message or "Failed to create crowdloan."
465
+
466
+ if json_output:
467
+ extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier()
468
+ output_dict = {
469
+ "success": True,
470
+ "error": None,
471
+ "data": {
472
+ "type": crowdloan_type,
473
+ "deposit": deposit.tao,
474
+ "min_contribution": min_contribution.tao,
475
+ "cap": cap.tao,
476
+ "duration": duration,
477
+ "end_block": end_block,
478
+ "extrinsic_id": extrinsic_id,
479
+ },
480
+ }
481
+
482
+ if crowdloan_type == "subnet":
483
+ output_dict["data"]["emissions_share"] = emissions_share
484
+ output_dict["data"]["lease_end_block"] = lease_end_block
485
+ output_dict["data"]["perpetual_lease"] = lease_end_block is None
486
+ elif crowdloan_type == "custom":
487
+ output_dict["data"]["custom_call"] = custom_call_info
488
+ else:
489
+ output_dict["data"]["target_address"] = target_address
490
+
491
+ json_console.print(json.dumps(output_dict))
492
+ message = f"{crowdloan_type.capitalize()} crowdloan created successfully."
493
+ else:
494
+ if crowdloan_type == "subnet":
495
+ message = "Subnet lease crowdloan created successfully."
496
+ print_success(message)
497
+ console.print(
498
+ f" Type: [magenta]Subnet Leasing[/magenta]\n"
499
+ f" Emissions Share: [cyan]{emissions_share}%[/cyan]\n"
500
+ f" Deposit: [{COLORS.P.MESH}]{deposit}[/{COLORS.P.MESH}]\n"
501
+ f" Min contribution: [{COLORS.P.MESH}]{min_contribution}[/{COLORS.P.MESH}]\n"
502
+ f" Cap: [{COLORS.P.MESH}]{cap}[/{COLORS.P.MESH}]\n"
503
+ f" Ends at block: [bold]{end_block}[/bold]"
504
+ )
505
+ if lease_end_block:
506
+ console.print(f" Lease ends at block: [bold]{lease_end_block}[/bold]")
507
+ else:
508
+ console.print(" Lease: [green]Perpetual[/green]")
509
+ elif crowdloan_type == "custom":
510
+ message = "Custom call crowdloan created successfully."
511
+ console.print(
512
+ f"\n:white_check_mark: [green]{message}[/green]\n"
513
+ f" Type: [yellow]Custom Call[/yellow]\n"
514
+ f" Pallet: [cyan]{custom_call_info['pallet']}[/cyan]\n"
515
+ f" Method: [cyan]{custom_call_info['method']}[/cyan]\n"
516
+ f" Deposit: [{COLORS.P.MESH}]{deposit}[/{COLORS.P.MESH}]\n"
517
+ f" Min contribution: [{COLORS.P.MESH}]{min_contribution}[/{COLORS.P.MESH}]\n"
518
+ f" Cap: [{COLORS.P.MESH}]{cap}[/{COLORS.P.MESH}]\n"
519
+ f" Ends at block: [bold]{end_block}[/bold]"
520
+ )
521
+ if custom_call_info["args"]:
522
+ args_str = json.dumps(custom_call_info["args"], indent=2)
523
+ console.print(f" Call Arguments:\n{args_str}")
524
+ else:
525
+ message = "Fundraising crowdloan created successfully."
526
+ print_success(message)
527
+ console.print(
528
+ f" Type: [cyan]General Fundraising[/cyan]\n"
529
+ f" Deposit: [{COLORS.P.MESH}]{deposit}[/{COLORS.P.MESH}]\n"
530
+ f" Min contribution: [{COLORS.P.MESH}]{min_contribution}[/{COLORS.P.MESH}]\n"
531
+ f" Cap: [{COLORS.P.MESH}]{cap}[/{COLORS.P.MESH}]\n"
532
+ f" Ends at block: [bold]{end_block}[/bold]"
533
+ )
534
+ if target_address:
535
+ console.print(f" Target address: {target_address}")
536
+
537
+ await print_extrinsic_id(extrinsic_receipt)
538
+
539
+ return True, message
540
+
541
+
542
+ async def finalize_crowdloan(
543
+ meshtensor: MeshtensorInterface,
544
+ wallet: Wallet,
545
+ proxy: Optional[str],
546
+ crowdloan_id: int,
547
+ wait_for_inclusion: bool,
548
+ wait_for_finalization: bool,
549
+ prompt: bool,
550
+ decline: bool = False,
551
+ quiet: bool = False,
552
+ json_output: bool = False,
553
+ ) -> tuple[bool, str]:
554
+ """
555
+ Finalize a successful crowdloan that has reached its cap.
556
+
557
+ Only the creator can finalize a crowdloan. Finalization will:
558
+ - Transfer funds to the target address (if specified)
559
+ - Execute the attached call (if any, e.g., subnet creation)
560
+ - Mark the crowdloan as finalized
561
+
562
+ Args:
563
+ meshtensor: MeshtensorInterface instance for blockchain interaction
564
+ wallet: Wallet instance containing the user's keys
565
+ proxy: Optional proxy to use for this extrinsic submission
566
+ crowdloan_id: The ID of the crowdloan to finalize
567
+ wait_for_inclusion: Whether to wait for transaction inclusion
568
+ wait_for_finalization: Whether to wait for transaction finalization
569
+ prompt: Whether to prompt for user confirmation
570
+ json_output: Whether to output the crowdloan info as JSON or human-readable
571
+
572
+ Returns:
573
+ Tuple of (success, message) indicating the result
574
+ """
575
+
576
+ crowdloan, current_block = await asyncio.gather(
577
+ meshtensor.get_single_crowdloan(crowdloan_id),
578
+ meshtensor.substrate.get_block_number(None),
579
+ )
580
+
581
+ if not crowdloan:
582
+ error_msg = f"Crowdloan #{crowdloan_id} does not exist."
583
+ if json_output:
584
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
585
+ else:
586
+ print_error(error_msg)
587
+ return False, error_msg
588
+
589
+ if wallet.coldkeypub.ss58_address != crowdloan.creator:
590
+ error_msg = (
591
+ f"Only the creator can finalize a crowdloan. Creator: {crowdloan.creator}"
592
+ )
593
+ if json_output:
594
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
595
+ else:
596
+ print_error(error_msg)
597
+ return False, "Only the creator can finalize a crowdloan."
598
+
599
+ if crowdloan.finalized:
600
+ error_msg = f"Crowdloan #{crowdloan_id} is already finalized."
601
+ if json_output:
602
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
603
+ else:
604
+ print_error(error_msg)
605
+ return False, "Crowdloan is already finalized."
606
+
607
+ if crowdloan.raised < crowdloan.cap:
608
+ still_needed = crowdloan.cap - crowdloan.raised
609
+ error_msg = (
610
+ f"Crowdloan #{crowdloan_id} has not reached its cap. Raised: {crowdloan.raised.tao}, "
611
+ f"Cap: {crowdloan.cap.tao}, Still needed: {still_needed.tao}"
612
+ )
613
+ if json_output:
614
+ json_console.print(json.dumps({"success": False, "error": error_msg}))
615
+ else:
616
+ print_error(
617
+ f"Crowdloan #{crowdloan_id} has not reached its cap.\n"
618
+ f"Raised: {crowdloan.raised}, Cap: {crowdloan.cap}\n"
619
+ f"Still needed: {still_needed.tao}"
620
+ )
621
+ return False, "Crowdloan has not reached its cap."
622
+
623
+ call = await meshtensor.substrate.compose_call(
624
+ call_module="Crowdloan",
625
+ call_function="finalize",
626
+ call_params={
627
+ "crowdloan_id": crowdloan_id,
628
+ },
629
+ )
630
+ extrinsic_fee = await meshtensor.get_extrinsic_fee(
631
+ call, wallet.coldkeypub, proxy=proxy
632
+ )
633
+
634
+ await show_crowdloan_details(
635
+ meshtensor=meshtensor,
636
+ crowdloan_id=crowdloan_id,
637
+ wallet=wallet,
638
+ verbose=False,
639
+ crowdloan=crowdloan,
640
+ current_block=current_block,
641
+ )
642
+
643
+ if prompt:
644
+ console.print()
645
+ table = Table(
646
+ Column("[bold white]Field", style=COLORS.G.SUBHEAD),
647
+ Column("[bold white]Value", style=COLORS.G.TEMPO),
648
+ title="\n[bold cyan]Crowdloan Finalization Summary[/bold cyan]",
649
+ show_footer=False,
650
+ show_header=False,
651
+ width=None,
652
+ pad_edge=False,
653
+ box=box.SIMPLE,
654
+ show_edge=True,
655
+ border_style="bright_black",
656
+ )
657
+
658
+ table.add_row("Crowdloan ID", str(crowdloan_id))
659
+ table.add_row("Status", "[green]Ready to Finalize[/green]")
660
+ table.add_row(
661
+ "Total Raised", f"[{COLORS.S.AMOUNT}]{crowdloan.raised}[/{COLORS.S.AMOUNT}]"
662
+ )
663
+ table.add_row("Contributors", str(crowdloan.contributors_count))
664
+
665
+ if crowdloan.target_address:
666
+ table.add_row(
667
+ "Funds Will Go To",
668
+ f"[{COLORS.G.SUBHEAD_EX_1}]{crowdloan.target_address}[/{COLORS.G.SUBHEAD_EX_1}]",
669
+ )
670
+
671
+ if crowdloan.has_call:
672
+ table.add_row(
673
+ "Call to Execute", "[yellow]Yes (e.g., subnet registration)[/yellow]"
674
+ )
675
+ else:
676
+ table.add_row("Call to Execute", "[dim]None[/dim]")
677
+
678
+ table.add_row(
679
+ "Transaction Fee",
680
+ f"[{COLORS.S.MESH}]{extrinsic_fee.tao}[/{COLORS.S.MESH}]"
681
+ + (" (paid by signer account)" if proxy else ""),
682
+ )
683
+
684
+ table.add_section()
685
+ table.add_row(
686
+ "[bold red]WARNING[/bold red]",
687
+ "[yellow]This action is IRREVERSIBLE![/yellow]",
688
+ )
689
+
690
+ console.print(table)
691
+
692
+ console.print(
693
+ "\n[bold yellow]Important:[/bold yellow]\n"
694
+ "• Finalization will transfer all raised funds\n"
695
+ "• Any attached call will be executed immediately\n"
696
+ "• This action cannot be undone\n"
697
+ )
698
+
699
+ if not confirm_action(
700
+ "\nProceed with finalization?", decline=decline, quiet=quiet
701
+ ):
702
+ if json_output:
703
+ json_console.print(
704
+ json.dumps(
705
+ {"success": False, "error": "Finalization cancelled by user."}
706
+ )
707
+ )
708
+ else:
709
+ console.print("[yellow]Finalization cancelled.[/yellow]")
710
+ return False, "Finalization cancelled by user."
711
+
712
+ unlock_status = unlock_key(wallet)
713
+ if not unlock_status.success:
714
+ if json_output:
715
+ json_console.print(
716
+ json.dumps({"success": False, "error": unlock_status.message})
717
+ )
718
+ else:
719
+ print_error(f"[red]{unlock_status.message}[/red]")
720
+ return False, unlock_status.message
721
+
722
+ success, error_message, extrinsic_receipt = await meshtensor.sign_and_send_extrinsic(
723
+ call=call,
724
+ wallet=wallet,
725
+ wait_for_inclusion=wait_for_inclusion,
726
+ wait_for_finalization=wait_for_finalization,
727
+ proxy=proxy,
728
+ )
729
+
730
+ if not success:
731
+ if json_output:
732
+ json_console.print(
733
+ json.dumps(
734
+ {
735
+ "success": False,
736
+ "error": error_message or "Failed to finalize crowdloan.",
737
+ }
738
+ )
739
+ )
740
+ else:
741
+ print_error(
742
+ f"[red]Failed to finalize: {error_message or 'Unknown error'}[/red]"
743
+ )
744
+ return False, error_message or "Failed to finalize crowdloan."
745
+
746
+ if json_output:
747
+ extrinsic_id = await extrinsic_receipt.get_extrinsic_identifier()
748
+ output_dict = {
749
+ "success": True,
750
+ "error": None,
751
+ "extrinsic_identifier": extrinsic_id,
752
+ "data": {
753
+ "crowdloan_id": crowdloan_id,
754
+ "total_raised": crowdloan.raised.tao,
755
+ "contributors_count": crowdloan.contributors_count,
756
+ "target_address": crowdloan.target_address,
757
+ "has_call": crowdloan.has_call,
758
+ "call_executed": crowdloan.has_call,
759
+ },
760
+ }
761
+ json_console.print(json.dumps(output_dict))
762
+ else:
763
+ console.print(
764
+ f"\n[dark_sea_green3]Successfully finalized crowdloan #{crowdloan_id}![/dark_sea_green3]\n"
765
+ )
766
+
767
+ console.print(
768
+ f"[bold]Finalization Complete:[/bold]\n"
769
+ f"\t• Total Raised: [{COLORS.S.AMOUNT}]{crowdloan.raised}[/{COLORS.S.AMOUNT}]\n"
770
+ f"\t• Contributors: {crowdloan.contributors_count}"
771
+ )
772
+
773
+ if crowdloan.target_address:
774
+ console.print(
775
+ f"\t• Funds transferred to: [{COLORS.G.SUBHEAD_EX_1}]{crowdloan.target_address}[/{COLORS.G.SUBHEAD_EX_1}]"
776
+ )
777
+
778
+ if crowdloan.has_call:
779
+ console.print("\t• [green]Associated call has been executed[/green]")
780
+
781
+ await print_extrinsic_id(extrinsic_receipt)
782
+
783
+ return True, "Successfully finalized crowdloan."