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.
- meshtensor_cli/__init__.py +22 -0
- meshtensor_cli/cli.py +10742 -0
- meshtensor_cli/doc_generation_helper.py +4 -0
- meshtensor_cli/src/__init__.py +1085 -0
- meshtensor_cli/src/commands/__init__.py +0 -0
- meshtensor_cli/src/commands/axon/__init__.py +0 -0
- meshtensor_cli/src/commands/axon/axon.py +132 -0
- meshtensor_cli/src/commands/crowd/__init__.py +0 -0
- meshtensor_cli/src/commands/crowd/contribute.py +621 -0
- meshtensor_cli/src/commands/crowd/contributors.py +200 -0
- meshtensor_cli/src/commands/crowd/create.py +783 -0
- meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
- meshtensor_cli/src/commands/crowd/refund.py +233 -0
- meshtensor_cli/src/commands/crowd/update.py +418 -0
- meshtensor_cli/src/commands/crowd/utils.py +124 -0
- meshtensor_cli/src/commands/crowd/view.py +991 -0
- meshtensor_cli/src/commands/governance/__init__.py +0 -0
- meshtensor_cli/src/commands/governance/governance.py +794 -0
- meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
- meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
- meshtensor_cli/src/commands/liquidity/utils.py +202 -0
- meshtensor_cli/src/commands/proxy.py +700 -0
- meshtensor_cli/src/commands/stake/__init__.py +0 -0
- meshtensor_cli/src/commands/stake/add.py +799 -0
- meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
- meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
- meshtensor_cli/src/commands/stake/claim.py +770 -0
- meshtensor_cli/src/commands/stake/list.py +738 -0
- meshtensor_cli/src/commands/stake/move.py +1211 -0
- meshtensor_cli/src/commands/stake/remove.py +1466 -0
- meshtensor_cli/src/commands/stake/wizard.py +323 -0
- meshtensor_cli/src/commands/subnets/__init__.py +0 -0
- meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
- meshtensor_cli/src/commands/subnets/price.py +733 -0
- meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
- meshtensor_cli/src/commands/sudo.py +1294 -0
- meshtensor_cli/src/commands/tc/__init__.py +0 -0
- meshtensor_cli/src/commands/tc/tc.py +190 -0
- meshtensor_cli/src/commands/treasury/__init__.py +0 -0
- meshtensor_cli/src/commands/treasury/treasury.py +194 -0
- meshtensor_cli/src/commands/view.py +354 -0
- meshtensor_cli/src/commands/wallets.py +2311 -0
- meshtensor_cli/src/commands/weights.py +467 -0
- meshtensor_cli/src/meshtensor/__init__.py +0 -0
- meshtensor_cli/src/meshtensor/balances.py +313 -0
- meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
- meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
- meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
- meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
- meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
- meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
- meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
- meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
- meshtensor_cli/src/meshtensor/minigraph.py +254 -0
- meshtensor_cli/src/meshtensor/networking.py +12 -0
- meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
- meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
- meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
- meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
- meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
- meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
- meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
- meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
- meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
- meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
- meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
- meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
- meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
- meshtensor_cli/src/meshtensor/utils.py +2007 -0
- meshtensor_cli/version.py +23 -0
- meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
- meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
- meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
- 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."
|