bittensor-cli 8.4.4__py3-none-any.whl → 9.0.0__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.
- bittensor_cli/__init__.py +1 -1
- bittensor_cli/cli.py +1827 -1394
- bittensor_cli/src/__init__.py +623 -168
- bittensor_cli/src/bittensor/balances.py +41 -8
- bittensor_cli/src/bittensor/chain_data.py +557 -428
- bittensor_cli/src/bittensor/extrinsics/registration.py +129 -23
- bittensor_cli/src/bittensor/extrinsics/root.py +3 -3
- bittensor_cli/src/bittensor/extrinsics/transfer.py +6 -11
- bittensor_cli/src/bittensor/minigraph.py +46 -8
- bittensor_cli/src/bittensor/subtensor_interface.py +567 -250
- bittensor_cli/src/bittensor/utils.py +370 -25
- bittensor_cli/src/commands/stake/__init__.py +154 -0
- bittensor_cli/src/commands/stake/add.py +625 -0
- bittensor_cli/src/commands/stake/children_hotkeys.py +103 -75
- bittensor_cli/src/commands/stake/list.py +687 -0
- bittensor_cli/src/commands/stake/move.py +1000 -0
- bittensor_cli/src/commands/stake/remove.py +1146 -0
- bittensor_cli/src/commands/subnets/__init__.py +0 -0
- bittensor_cli/src/commands/subnets/price.py +867 -0
- bittensor_cli/src/commands/subnets/subnets.py +2028 -0
- bittensor_cli/src/commands/sudo.py +554 -12
- bittensor_cli/src/commands/wallets.py +225 -531
- bittensor_cli/src/commands/weights.py +2 -2
- {bittensor_cli-8.4.4.dist-info → bittensor_cli-9.0.0.dist-info}/METADATA +7 -4
- bittensor_cli-9.0.0.dist-info/RECORD +34 -0
- bittensor_cli/src/bittensor/async_substrate_interface.py +0 -2748
- bittensor_cli/src/commands/root.py +0 -1787
- bittensor_cli/src/commands/stake/stake.py +0 -1448
- bittensor_cli/src/commands/subnets.py +0 -897
- bittensor_cli-8.4.4.dist-info/RECORD +0 -31
- {bittensor_cli-8.4.4.dist-info → bittensor_cli-9.0.0.dist-info}/WHEEL +0 -0
- {bittensor_cli-8.4.4.dist-info → bittensor_cli-9.0.0.dist-info}/entry_points.txt +0 -0
- {bittensor_cli-8.4.4.dist-info → bittensor_cli-9.0.0.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,14 @@
|
|
1
1
|
import asyncio
|
2
|
-
from typing import TYPE_CHECKING, Union
|
2
|
+
from typing import TYPE_CHECKING, Union, Optional
|
3
3
|
|
4
|
+
import typer
|
4
5
|
from bittensor_wallet import Wallet
|
5
6
|
from rich import box
|
6
7
|
from rich.table import Column, Table
|
8
|
+
from rich.prompt import Confirm
|
9
|
+
from scalecodec import GenericCall
|
7
10
|
|
8
|
-
from bittensor_cli.src import HYPERPARAMS
|
11
|
+
from bittensor_cli.src import HYPERPARAMS, DelegatesDetails, COLOR_PALETTE
|
9
12
|
from bittensor_cli.src.bittensor.chain_data import decode_account_id
|
10
13
|
from bittensor_cli.src.bittensor.utils import (
|
11
14
|
console,
|
@@ -14,10 +17,14 @@ from bittensor_cli.src.bittensor.utils import (
|
|
14
17
|
print_verbose,
|
15
18
|
normalize_hyperparameters,
|
16
19
|
unlock_key,
|
20
|
+
blocks_to_duration,
|
17
21
|
)
|
18
22
|
|
19
23
|
if TYPE_CHECKING:
|
20
|
-
from bittensor_cli.src.bittensor.subtensor_interface import
|
24
|
+
from bittensor_cli.src.bittensor.subtensor_interface import (
|
25
|
+
SubtensorInterface,
|
26
|
+
ProposalVoteData,
|
27
|
+
)
|
21
28
|
|
22
29
|
|
23
30
|
# helpers and extrinsics
|
@@ -89,12 +96,11 @@ async def set_hyperparameter_extrinsic(
|
|
89
96
|
finalization/inclusion, the response is `True`.
|
90
97
|
"""
|
91
98
|
print_verbose("Confirming subnet owner")
|
92
|
-
|
99
|
+
subnet_owner = await subtensor.query(
|
93
100
|
module="SubtensorModule",
|
94
101
|
storage_function="SubnetOwner",
|
95
102
|
params=[netuid],
|
96
103
|
)
|
97
|
-
subnet_owner = decode_account_id(subnet_owner_[0])
|
98
104
|
if subnet_owner != wallet.coldkeypub.ss58_address:
|
99
105
|
err_console.print(
|
100
106
|
":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]"
|
@@ -110,7 +116,7 @@ async def set_hyperparameter_extrinsic(
|
|
110
116
|
return False
|
111
117
|
|
112
118
|
with console.status(
|
113
|
-
f":satellite: Setting hyperparameter {parameter} to {value} on subnet: {netuid} ...",
|
119
|
+
f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{value}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...",
|
114
120
|
spinner="earth",
|
115
121
|
):
|
116
122
|
substrate = subtensor.substrate
|
@@ -165,11 +171,297 @@ async def set_hyperparameter_extrinsic(
|
|
165
171
|
# Successful registration, final check for membership
|
166
172
|
else:
|
167
173
|
console.print(
|
168
|
-
f":white_heavy_check_mark: [
|
174
|
+
f":white_heavy_check_mark: [dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]"
|
169
175
|
)
|
170
176
|
return True
|
171
177
|
|
172
178
|
|
179
|
+
async def _get_senate_members(
|
180
|
+
subtensor: "SubtensorInterface", block_hash: Optional[str] = None
|
181
|
+
) -> list[str]:
|
182
|
+
"""
|
183
|
+
Gets all members of the senate on the given subtensor's network
|
184
|
+
|
185
|
+
:param subtensor: SubtensorInterface object to use for the query
|
186
|
+
|
187
|
+
:return: list of the senate members' ss58 addresses
|
188
|
+
"""
|
189
|
+
senate_members = await subtensor.query(
|
190
|
+
module="SenateMembers",
|
191
|
+
storage_function="Members",
|
192
|
+
params=None,
|
193
|
+
block_hash=block_hash,
|
194
|
+
)
|
195
|
+
try:
|
196
|
+
return [
|
197
|
+
decode_account_id(i[x][0]) for i in senate_members for x in range(len(i))
|
198
|
+
]
|
199
|
+
except (IndexError, TypeError):
|
200
|
+
err_console.print("Unable to retrieve senate members.")
|
201
|
+
return []
|
202
|
+
|
203
|
+
|
204
|
+
async def _get_proposals(
|
205
|
+
subtensor: "SubtensorInterface", block_hash: str
|
206
|
+
) -> dict[str, tuple[dict, "ProposalVoteData"]]:
|
207
|
+
async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]:
|
208
|
+
proposal_data = await subtensor.query(
|
209
|
+
module="Triumvirate",
|
210
|
+
storage_function="ProposalOf",
|
211
|
+
block_hash=block_hash,
|
212
|
+
params=[p_hash],
|
213
|
+
)
|
214
|
+
return proposal_data
|
215
|
+
|
216
|
+
ph = await subtensor.query(
|
217
|
+
module="Triumvirate",
|
218
|
+
storage_function="Proposals",
|
219
|
+
params=None,
|
220
|
+
block_hash=block_hash,
|
221
|
+
)
|
222
|
+
|
223
|
+
try:
|
224
|
+
proposal_hashes: list[str] = [
|
225
|
+
f"0x{bytes(ph[0][x][0]).hex()}" for x in range(len(ph[0]))
|
226
|
+
]
|
227
|
+
except (IndexError, TypeError):
|
228
|
+
err_console.print("Unable to retrieve proposal vote data")
|
229
|
+
return {}
|
230
|
+
|
231
|
+
call_data_, vote_data_ = await asyncio.gather(
|
232
|
+
asyncio.gather(*[get_proposal_call_data(h) for h in proposal_hashes]),
|
233
|
+
asyncio.gather(*[subtensor.get_vote_data(h) for h in proposal_hashes]),
|
234
|
+
)
|
235
|
+
return {
|
236
|
+
proposal_hash: (cd, vd)
|
237
|
+
for cd, vd, proposal_hash in zip(call_data_, vote_data_, proposal_hashes)
|
238
|
+
}
|
239
|
+
|
240
|
+
|
241
|
+
def display_votes(
|
242
|
+
vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails]
|
243
|
+
) -> str:
|
244
|
+
vote_list = list()
|
245
|
+
|
246
|
+
for address in vote_data.ayes:
|
247
|
+
vote_list.append(
|
248
|
+
"{}: {}".format(
|
249
|
+
delegate_info[address].display if address in delegate_info else address,
|
250
|
+
"[bold green]Aye[/bold green]",
|
251
|
+
)
|
252
|
+
)
|
253
|
+
|
254
|
+
for address in vote_data.nays:
|
255
|
+
vote_list.append(
|
256
|
+
"{}: {}".format(
|
257
|
+
delegate_info[address].display if address in delegate_info else address,
|
258
|
+
"[bold red]Nay[/bold red]",
|
259
|
+
)
|
260
|
+
)
|
261
|
+
|
262
|
+
return "\n".join(vote_list)
|
263
|
+
|
264
|
+
|
265
|
+
def format_call_data(call_data: dict) -> str:
|
266
|
+
# Extract the module and call details
|
267
|
+
module, call_details = next(iter(call_data.items()))
|
268
|
+
|
269
|
+
# Extract the call function name and arguments
|
270
|
+
call_info = call_details[0]
|
271
|
+
call_function, call_args = next(iter(call_info.items()))
|
272
|
+
|
273
|
+
# Format arguments, handle nested/large payloads
|
274
|
+
formatted_args = []
|
275
|
+
for arg_name, arg_value in call_args.items():
|
276
|
+
if isinstance(arg_value, (tuple, list, dict)):
|
277
|
+
# For large nested, show abbreviated version
|
278
|
+
content_str = str(arg_value)
|
279
|
+
if len(content_str) > 20:
|
280
|
+
formatted_args.append(f"{arg_name}: ... [{len(content_str)}] ...")
|
281
|
+
else:
|
282
|
+
formatted_args.append(f"{arg_name}: {arg_value}")
|
283
|
+
else:
|
284
|
+
formatted_args.append(f"{arg_name}: {arg_value}")
|
285
|
+
|
286
|
+
# Format the final output string
|
287
|
+
args_str = ", ".join(formatted_args)
|
288
|
+
return f"{module}.{call_function}({args_str})"
|
289
|
+
|
290
|
+
|
291
|
+
def _validate_proposal_hash(proposal_hash: str) -> bool:
|
292
|
+
if proposal_hash[0:2] != "0x" or len(proposal_hash) != 66:
|
293
|
+
return False
|
294
|
+
else:
|
295
|
+
return True
|
296
|
+
|
297
|
+
|
298
|
+
async def _is_senate_member(subtensor: "SubtensorInterface", hotkey_ss58: str) -> bool:
|
299
|
+
"""
|
300
|
+
Checks if a given neuron (identified by its hotkey SS58 address) is a member of the Bittensor senate.
|
301
|
+
The senate is a key governance body within the Bittensor network, responsible for overseeing and
|
302
|
+
approving various network operations and proposals.
|
303
|
+
|
304
|
+
:param subtensor: SubtensorInterface object to use for the query
|
305
|
+
:param hotkey_ss58: The `SS58` address of the neuron's hotkey.
|
306
|
+
|
307
|
+
:return: `True` if the neuron is a senate member at the given block, `False` otherwise.
|
308
|
+
|
309
|
+
This function is crucial for understanding the governance dynamics of the Bittensor network and for
|
310
|
+
identifying the neurons that hold decision-making power within the network.
|
311
|
+
"""
|
312
|
+
|
313
|
+
senate_members = await _get_senate_members(subtensor)
|
314
|
+
|
315
|
+
if not hasattr(senate_members, "count"):
|
316
|
+
return False
|
317
|
+
|
318
|
+
return senate_members.count(hotkey_ss58) > 0
|
319
|
+
|
320
|
+
|
321
|
+
async def vote_senate_extrinsic(
|
322
|
+
subtensor: "SubtensorInterface",
|
323
|
+
wallet: Wallet,
|
324
|
+
proposal_hash: str,
|
325
|
+
proposal_idx: int,
|
326
|
+
vote: bool,
|
327
|
+
wait_for_inclusion: bool = False,
|
328
|
+
wait_for_finalization: bool = True,
|
329
|
+
prompt: bool = False,
|
330
|
+
) -> bool:
|
331
|
+
"""Votes ayes or nays on proposals.
|
332
|
+
|
333
|
+
:param subtensor: The SubtensorInterface object to use for the query
|
334
|
+
:param wallet: Bittensor wallet object, with coldkey and hotkey unlocked.
|
335
|
+
:param proposal_hash: The hash of the proposal for which voting data is requested.
|
336
|
+
:param proposal_idx: The index of the proposal to vote.
|
337
|
+
:param vote: Whether to vote aye or nay.
|
338
|
+
:param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns
|
339
|
+
`False` if the extrinsic fails to enter the block within the timeout.
|
340
|
+
:param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
|
341
|
+
or returns `False` if the extrinsic fails to be finalized within the timeout.
|
342
|
+
:param prompt: If `True`, the call waits for confirmation from the user before proceeding.
|
343
|
+
|
344
|
+
:return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for
|
345
|
+
finalization/inclusion, the response is `True`.
|
346
|
+
"""
|
347
|
+
|
348
|
+
if prompt:
|
349
|
+
# Prompt user for confirmation.
|
350
|
+
if not Confirm.ask(f"Cast a vote of {vote}?"):
|
351
|
+
return False
|
352
|
+
|
353
|
+
with console.status(":satellite: Casting vote..", spinner="aesthetic"):
|
354
|
+
call = await subtensor.substrate.compose_call(
|
355
|
+
call_module="SubtensorModule",
|
356
|
+
call_function="vote",
|
357
|
+
call_params={
|
358
|
+
"hotkey": wallet.hotkey.ss58_address,
|
359
|
+
"proposal": proposal_hash,
|
360
|
+
"index": proposal_idx,
|
361
|
+
"approve": vote,
|
362
|
+
},
|
363
|
+
)
|
364
|
+
success, err_msg = await subtensor.sign_and_send_extrinsic(
|
365
|
+
call, wallet, wait_for_inclusion, wait_for_finalization
|
366
|
+
)
|
367
|
+
if not success:
|
368
|
+
err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
|
369
|
+
await asyncio.sleep(0.5)
|
370
|
+
return False
|
371
|
+
# Successful vote, final check for data
|
372
|
+
else:
|
373
|
+
if vote_data := await subtensor.get_vote_data(proposal_hash):
|
374
|
+
if (
|
375
|
+
vote_data.ayes.count(wallet.hotkey.ss58_address) > 0
|
376
|
+
or vote_data.nays.count(wallet.hotkey.ss58_address) > 0
|
377
|
+
):
|
378
|
+
console.print(":white_heavy_check_mark: [green]Vote cast.[/green]")
|
379
|
+
return True
|
380
|
+
else:
|
381
|
+
# hotkey not found in ayes/nays
|
382
|
+
err_console.print(
|
383
|
+
":cross_mark: [red]Unknown error. Couldn't find vote.[/red]"
|
384
|
+
)
|
385
|
+
return False
|
386
|
+
else:
|
387
|
+
return False
|
388
|
+
|
389
|
+
|
390
|
+
async def set_take_extrinsic(
|
391
|
+
subtensor: "SubtensorInterface",
|
392
|
+
wallet: Wallet,
|
393
|
+
delegate_ss58: str,
|
394
|
+
take: float = 0.0,
|
395
|
+
) -> bool:
|
396
|
+
"""
|
397
|
+
Set delegate hotkey take
|
398
|
+
|
399
|
+
:param subtensor: SubtensorInterface (initialized)
|
400
|
+
:param wallet: The wallet containing the hotkey to be nominated.
|
401
|
+
:param delegate_ss58: Hotkey
|
402
|
+
:param take: Delegate take on subnet ID
|
403
|
+
|
404
|
+
:return: `True` if the process is successful, `False` otherwise.
|
405
|
+
|
406
|
+
This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the
|
407
|
+
dynamic selection and participation of validators in the network's consensus process.
|
408
|
+
"""
|
409
|
+
|
410
|
+
# Calculate u16 representation of the take
|
411
|
+
take_u16 = int(take * 0xFFFF)
|
412
|
+
|
413
|
+
print_verbose("Checking current take")
|
414
|
+
# Check if the new take is greater or lower than existing take or if existing is set
|
415
|
+
current_take = await get_current_take(subtensor, wallet)
|
416
|
+
current_take_u16 = int(float(current_take) * 0xFFFF)
|
417
|
+
|
418
|
+
if take_u16 == current_take_u16:
|
419
|
+
console.print("Nothing to do, take hasn't changed")
|
420
|
+
return True
|
421
|
+
|
422
|
+
if current_take_u16 < take_u16:
|
423
|
+
console.print(
|
424
|
+
f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%[/{COLOR_PALETTE['POOLS']['RATE']}]. Increasing to [{COLOR_PALETTE['POOLS']['RATE']}]{take * 100:.2f}%."
|
425
|
+
)
|
426
|
+
with console.status(
|
427
|
+
f":satellite: Sending decrease_take_extrinsic call on [white]{subtensor}[/white] ..."
|
428
|
+
):
|
429
|
+
call = await subtensor.substrate.compose_call(
|
430
|
+
call_module="SubtensorModule",
|
431
|
+
call_function="increase_take",
|
432
|
+
call_params={
|
433
|
+
"hotkey": delegate_ss58,
|
434
|
+
"take": take_u16,
|
435
|
+
},
|
436
|
+
)
|
437
|
+
success, err = await subtensor.sign_and_send_extrinsic(call, wallet)
|
438
|
+
|
439
|
+
else:
|
440
|
+
console.print(
|
441
|
+
f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%[/{COLOR_PALETTE['POOLS']['RATE']}]. Decreasing to [{COLOR_PALETTE['POOLS']['RATE']}]{take * 100:.2f}%."
|
442
|
+
)
|
443
|
+
with console.status(
|
444
|
+
f":satellite: Sending increase_take_extrinsic call on [white]{subtensor}[/white] ..."
|
445
|
+
):
|
446
|
+
call = await subtensor.substrate.compose_call(
|
447
|
+
call_module="SubtensorModule",
|
448
|
+
call_function="decrease_take",
|
449
|
+
call_params={
|
450
|
+
"hotkey": delegate_ss58,
|
451
|
+
"take": take_u16,
|
452
|
+
},
|
453
|
+
)
|
454
|
+
success, err = await subtensor.sign_and_send_extrinsic(call, wallet)
|
455
|
+
|
456
|
+
if not success:
|
457
|
+
err_console.print(err)
|
458
|
+
else:
|
459
|
+
console.print(
|
460
|
+
":white_heavy_check_mark: [dark_sea_green_3]Finalized[/dark_sea_green_3]"
|
461
|
+
)
|
462
|
+
return success
|
463
|
+
|
464
|
+
|
173
465
|
# commands
|
174
466
|
|
175
467
|
|
@@ -217,13 +509,20 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int):
|
|
217
509
|
print_error(f"Subnet with netuid {netuid} does not exist.")
|
218
510
|
return False
|
219
511
|
subnet = await subtensor.get_subnet_hyperparameters(netuid)
|
512
|
+
subnet_info = await subtensor.subnet(netuid)
|
513
|
+
if subnet_info is None:
|
514
|
+
print_error(f"Subnet with netuid {netuid} does not exist.")
|
515
|
+
raise typer.Exit()
|
220
516
|
|
221
517
|
table = Table(
|
222
|
-
Column("[white]HYPERPARAMETER", style="
|
223
|
-
Column("[white]VALUE", style="
|
224
|
-
Column("[white]NORMALIZED", style="
|
225
|
-
title=f"[
|
226
|
-
f"{
|
518
|
+
Column("[white]HYPERPARAMETER", style=COLOR_PALETTE["SUDO"]["HYPERPARAMETER"]),
|
519
|
+
Column("[white]VALUE", style=COLOR_PALETTE["SUDO"]["VALUE"]),
|
520
|
+
Column("[white]NORMALIZED", style=COLOR_PALETTE["SUDO"]["NORMALIZED"]),
|
521
|
+
title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]\nSubnet Hyperparameters\n NETUID: "
|
522
|
+
f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}"
|
523
|
+
f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}"
|
524
|
+
f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
|
525
|
+
f" - Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n",
|
227
526
|
show_footer=True,
|
228
527
|
width=None,
|
229
528
|
pad_edge=False,
|
@@ -238,3 +537,246 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int):
|
|
238
537
|
|
239
538
|
console.print(table)
|
240
539
|
return True
|
540
|
+
|
541
|
+
|
542
|
+
async def get_senate(subtensor: "SubtensorInterface"):
|
543
|
+
"""View Bittensor's senate memebers"""
|
544
|
+
with console.status(
|
545
|
+
f":satellite: Syncing with chain: [white]{subtensor}[/white] ...",
|
546
|
+
spinner="aesthetic",
|
547
|
+
) as status:
|
548
|
+
print_verbose("Fetching senate members", status)
|
549
|
+
senate_members = await _get_senate_members(subtensor)
|
550
|
+
|
551
|
+
print_verbose("Fetching member details from Github and on-chain identities")
|
552
|
+
delegate_info: dict[
|
553
|
+
str, DelegatesDetails
|
554
|
+
] = await subtensor.get_delegate_identities()
|
555
|
+
|
556
|
+
table = Table(
|
557
|
+
Column(
|
558
|
+
"[bold white]NAME",
|
559
|
+
style="bright_cyan",
|
560
|
+
no_wrap=True,
|
561
|
+
),
|
562
|
+
Column(
|
563
|
+
"[bold white]ADDRESS",
|
564
|
+
style="bright_magenta",
|
565
|
+
no_wrap=True,
|
566
|
+
),
|
567
|
+
title=f"[underline dark_orange]Senate[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n",
|
568
|
+
show_footer=True,
|
569
|
+
show_edge=False,
|
570
|
+
expand=False,
|
571
|
+
border_style="bright_black",
|
572
|
+
leading=True,
|
573
|
+
)
|
574
|
+
|
575
|
+
for ss58_address in senate_members:
|
576
|
+
table.add_row(
|
577
|
+
(
|
578
|
+
delegate_info[ss58_address].display
|
579
|
+
if ss58_address in delegate_info
|
580
|
+
else "~"
|
581
|
+
),
|
582
|
+
ss58_address,
|
583
|
+
)
|
584
|
+
|
585
|
+
return console.print(table)
|
586
|
+
|
587
|
+
|
588
|
+
async def proposals(subtensor: "SubtensorInterface", verbose: bool):
|
589
|
+
console.print(
|
590
|
+
":satellite: Syncing with chain: [white]{}[/white] ...".format(
|
591
|
+
subtensor.network
|
592
|
+
)
|
593
|
+
)
|
594
|
+
block_hash = await subtensor.substrate.get_chain_head()
|
595
|
+
senate_members, all_proposals, current_block = await asyncio.gather(
|
596
|
+
_get_senate_members(subtensor, block_hash),
|
597
|
+
_get_proposals(subtensor, block_hash),
|
598
|
+
subtensor.substrate.get_block_number(block_hash),
|
599
|
+
)
|
600
|
+
|
601
|
+
registered_delegate_info: dict[
|
602
|
+
str, DelegatesDetails
|
603
|
+
] = await subtensor.get_delegate_identities()
|
604
|
+
|
605
|
+
title = (
|
606
|
+
f"[bold #4196D6]Bittensor Governance Proposals[/bold #4196D6]\n"
|
607
|
+
f"[steel_blue3]Current Block:[/steel_blue3] {current_block}\t"
|
608
|
+
f"[steel_blue3]Network:[/steel_blue3] {subtensor.network}\n\n"
|
609
|
+
f"[steel_blue3]Active Proposals:[/steel_blue3] {len(all_proposals)}\t"
|
610
|
+
f"[steel_blue3]Senate Size:[/steel_blue3] {len(senate_members)}\n"
|
611
|
+
)
|
612
|
+
table = Table(
|
613
|
+
Column(
|
614
|
+
"[white]HASH",
|
615
|
+
style="light_goldenrod2",
|
616
|
+
no_wrap=True,
|
617
|
+
),
|
618
|
+
Column("[white]THRESHOLD", style="rgb(42,161,152)"),
|
619
|
+
Column("[white]AYES", style="green"),
|
620
|
+
Column("[white]NAYS", style="red"),
|
621
|
+
Column(
|
622
|
+
"[white]VOTES",
|
623
|
+
style="rgb(50,163,219)",
|
624
|
+
),
|
625
|
+
Column("[white]END", style="bright_cyan"),
|
626
|
+
Column("[white]CALLDATA", style="dark_sea_green", width=30),
|
627
|
+
title=title,
|
628
|
+
show_footer=True,
|
629
|
+
box=box.SIMPLE_HEAVY,
|
630
|
+
pad_edge=False,
|
631
|
+
width=None,
|
632
|
+
border_style="bright_black",
|
633
|
+
)
|
634
|
+
for hash_, (call_data, vote_data) in all_proposals.items():
|
635
|
+
blocks_remaining = vote_data.end - current_block
|
636
|
+
if blocks_remaining > 0:
|
637
|
+
duration_str = blocks_to_duration(blocks_remaining)
|
638
|
+
vote_end_cell = f"{vote_data.end} [dim](in {duration_str})[/dim]"
|
639
|
+
else:
|
640
|
+
vote_end_cell = f"{vote_data.end} [red](expired)[/red]"
|
641
|
+
|
642
|
+
ayes_threshold = (
|
643
|
+
(len(vote_data.ayes) / vote_data.threshold * 100)
|
644
|
+
if vote_data.threshold > 0
|
645
|
+
else 0
|
646
|
+
)
|
647
|
+
nays_threshold = (
|
648
|
+
(len(vote_data.nays) / vote_data.threshold * 100)
|
649
|
+
if vote_data.threshold > 0
|
650
|
+
else 0
|
651
|
+
)
|
652
|
+
table.add_row(
|
653
|
+
hash_ if verbose else f"{hash_[:4]}...{hash_[-4:]}",
|
654
|
+
str(vote_data.threshold),
|
655
|
+
f"{len(vote_data.ayes)} ({ayes_threshold:.2f}%)",
|
656
|
+
f"{len(vote_data.nays)} ({nays_threshold:.2f}%)",
|
657
|
+
display_votes(vote_data, registered_delegate_info),
|
658
|
+
vote_end_cell,
|
659
|
+
format_call_data(call_data),
|
660
|
+
)
|
661
|
+
console.print(table)
|
662
|
+
console.print(
|
663
|
+
"\n[dim]* Both Ayes and Nays percentages are calculated relative to the proposal's threshold.[/dim]"
|
664
|
+
)
|
665
|
+
|
666
|
+
|
667
|
+
async def senate_vote(
|
668
|
+
wallet: Wallet,
|
669
|
+
subtensor: "SubtensorInterface",
|
670
|
+
proposal_hash: str,
|
671
|
+
vote: bool,
|
672
|
+
prompt: bool,
|
673
|
+
) -> bool:
|
674
|
+
"""Vote in Bittensor's governance protocol proposals"""
|
675
|
+
|
676
|
+
if not proposal_hash:
|
677
|
+
err_console.print(
|
678
|
+
"Aborting: Proposal hash not specified. View all proposals with the `proposals` command."
|
679
|
+
)
|
680
|
+
return False
|
681
|
+
elif not _validate_proposal_hash(proposal_hash):
|
682
|
+
err_console.print(
|
683
|
+
"Aborting. Proposal hash is invalid. Proposal hashes should start with '0x' and be 32 bytes long"
|
684
|
+
)
|
685
|
+
return False
|
686
|
+
|
687
|
+
print_verbose(f"Fetching senate status of {wallet.hotkey_str}")
|
688
|
+
if not await _is_senate_member(subtensor, hotkey_ss58=wallet.hotkey.ss58_address):
|
689
|
+
err_console.print(
|
690
|
+
f"Aborting: Hotkey {wallet.hotkey.ss58_address} isn't a senate member."
|
691
|
+
)
|
692
|
+
return False
|
693
|
+
|
694
|
+
# Unlock the wallet.
|
695
|
+
if (
|
696
|
+
not unlock_key(wallet, "hot").success
|
697
|
+
and unlock_key(wallet, "cold").success
|
698
|
+
):
|
699
|
+
return False
|
700
|
+
|
701
|
+
console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}")
|
702
|
+
vote_data = await subtensor.get_vote_data(proposal_hash, reuse_block=True)
|
703
|
+
if not vote_data:
|
704
|
+
err_console.print(":cross_mark: [red]Failed[/red]: Proposal not found.")
|
705
|
+
return False
|
706
|
+
|
707
|
+
success = await vote_senate_extrinsic(
|
708
|
+
subtensor=subtensor,
|
709
|
+
wallet=wallet,
|
710
|
+
proposal_hash=proposal_hash,
|
711
|
+
proposal_idx=vote_data.index,
|
712
|
+
vote=vote,
|
713
|
+
wait_for_inclusion=True,
|
714
|
+
wait_for_finalization=False,
|
715
|
+
prompt=prompt,
|
716
|
+
)
|
717
|
+
|
718
|
+
return success
|
719
|
+
|
720
|
+
|
721
|
+
async def get_current_take(subtensor: "SubtensorInterface", wallet: Wallet):
|
722
|
+
current_take = await subtensor.current_take(wallet.hotkey.ss58_address)
|
723
|
+
return current_take
|
724
|
+
|
725
|
+
|
726
|
+
async def display_current_take(subtensor: "SubtensorInterface", wallet: Wallet) -> None:
|
727
|
+
current_take = await get_current_take(subtensor, wallet)
|
728
|
+
console.print(
|
729
|
+
f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%"
|
730
|
+
)
|
731
|
+
|
732
|
+
|
733
|
+
async def set_take(
|
734
|
+
wallet: Wallet, subtensor: "SubtensorInterface", take: float
|
735
|
+
) -> bool:
|
736
|
+
"""Set delegate take."""
|
737
|
+
|
738
|
+
async def _do_set_take() -> bool:
|
739
|
+
if take > 0.18 or take < 0:
|
740
|
+
err_console.print("ERROR: Take value should not exceed 18% or be below 0%")
|
741
|
+
return False
|
742
|
+
|
743
|
+
block_hash = await subtensor.substrate.get_chain_head()
|
744
|
+
netuids_registered = await subtensor.get_netuids_for_hotkey(
|
745
|
+
wallet.hotkey.ss58_address, block_hash=block_hash
|
746
|
+
)
|
747
|
+
if not len(netuids_registered) > 0:
|
748
|
+
err_console.print(
|
749
|
+
f"Hotkey [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}] is not registered to any subnet. Please register using [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]`btcli subnets register`[{COLOR_PALETTE['GENERAL']['SUBHEADING']}] and try again."
|
750
|
+
)
|
751
|
+
return False
|
752
|
+
|
753
|
+
result: bool = await set_take_extrinsic(
|
754
|
+
subtensor=subtensor,
|
755
|
+
wallet=wallet,
|
756
|
+
delegate_ss58=wallet.hotkey.ss58_address,
|
757
|
+
take=take,
|
758
|
+
)
|
759
|
+
|
760
|
+
if not result:
|
761
|
+
err_console.print("Could not set the take")
|
762
|
+
return False
|
763
|
+
else:
|
764
|
+
new_take = await get_current_take(subtensor, wallet)
|
765
|
+
console.print(
|
766
|
+
f"New take is [{COLOR_PALETTE['POOLS']['RATE']}]{new_take * 100.:.2f}%"
|
767
|
+
)
|
768
|
+
return True
|
769
|
+
|
770
|
+
console.print(
|
771
|
+
f"Setting take on [{COLOR_PALETTE['GENERAL']['LINKS']}]network: {subtensor.network}"
|
772
|
+
)
|
773
|
+
|
774
|
+
if (
|
775
|
+
not unlock_key(wallet, "hot").success
|
776
|
+
and unlock_key(wallet, "cold").success
|
777
|
+
):
|
778
|
+
return False
|
779
|
+
|
780
|
+
result_ = await _do_set_take()
|
781
|
+
|
782
|
+
return result_
|