Skip to content

Commit 39e232a

Browse files
committed
Adds move_stake, transfer_stake, swap_stake to sync and async subtensor
1 parent 56f37de commit 39e232a

File tree

4 files changed

+628
-3
lines changed

4 files changed

+628
-3
lines changed

bittensor/core/async_subtensor.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,268 @@ async def add_stake(
526526

527527
stake = add_stake
528528

529+
async def transfer_stake(
530+
self,
531+
wallet: "Wallet",
532+
destination_coldkey_ss58: str,
533+
hotkey_ss58: str,
534+
origin_netuid: int,
535+
destination_netuid: int,
536+
amount: Union["Balance", float, int],
537+
wait_for_inclusion: bool = True,
538+
wait_for_finalization: bool = False,
539+
) -> bool:
540+
"""
541+
Transfers stake from one subnet to another. Keeps the same hotkey but destination coldkey is different.
542+
Allows moving stake to a different coldkey's control while also having the option to change the subnet.
543+
544+
Hotkey is the same. Coldkeys are different.
545+
546+
Args:
547+
wallet (bittensor.wallet): The wallet to transfer stake from.
548+
destination_coldkey_ss58 (str): The destination coldkey SS58 address. Different from the origin coldkey.
549+
hotkey_ss58 (str): The hotkey SS58 address associated with the stake. This is owned by the origin coldkey.
550+
origin_netuid (int): The source subnet UID.
551+
destination_netuid (int): The destination subnet UID.
552+
amount (Union[Balance, float]): Amount to transfer.
553+
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
554+
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
555+
556+
Returns:
557+
success (bool): True if the extrinsic was included in a block.
558+
559+
Raises:
560+
StakeError: If the transfer fails due to insufficient stake or other reasons.
561+
"""
562+
if isinstance(amount, (float, int)):
563+
amount = Balance.from_tao(amount)
564+
565+
hotkey_owner = await self.get_hotkey_owner(hotkey_ss58)
566+
if hotkey_owner != wallet.coldkeypub.ss58_address:
567+
logging.error(
568+
f":cross_mark: [red]Failed[/red]: Hotkey: {hotkey_ss58} does not belong to the origin coldkey owner: {wallet.coldkeypub.ss58_address}"
569+
)
570+
return False
571+
572+
stake_in_origin = await self.get_stake(
573+
hotkey_ss58=hotkey_ss58,
574+
coldkey_ss58=wallet.coldkeypub.ss58_address,
575+
netuid=origin_netuid,
576+
)
577+
if stake_in_origin < amount:
578+
logging.error(
579+
f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}"
580+
)
581+
return False
582+
583+
call = await self.substrate.compose_call(
584+
call_module="SubtensorModule",
585+
call_function="transfer_stake",
586+
call_params={
587+
"destination_coldkey": destination_coldkey_ss58,
588+
"hotkey": hotkey_ss58,
589+
"origin_netuid": origin_netuid,
590+
"destination_netuid": destination_netuid,
591+
"alpha_amount": amount.rao,
592+
},
593+
)
594+
next_nonce = await self.substrate.get_account_next_index(
595+
wallet.coldkeypub.ss58_address
596+
)
597+
extrinsic = await self.substrate.create_signed_extrinsic(
598+
call=call, keypair=wallet.coldkey, nonce=next_nonce
599+
)
600+
response = await self.substrate.submit_extrinsic(
601+
extrinsic,
602+
wait_for_inclusion=wait_for_inclusion,
603+
wait_for_finalization=wait_for_finalization,
604+
)
605+
if not wait_for_finalization and not wait_for_inclusion:
606+
return True
607+
608+
if await response.is_success:
609+
return True
610+
else:
611+
logging.error(
612+
f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}"
613+
)
614+
return False
615+
616+
async def swap_stake(
617+
self,
618+
wallet: "Wallet",
619+
hotkey_ss58: str,
620+
origin_netuid: int,
621+
destination_netuid: int,
622+
amount: Union["Balance", float, int],
623+
wait_for_inclusion: bool = True,
624+
wait_for_finalization: bool = False,
625+
) -> bool:
626+
"""
627+
Moves stake between subnets while keeping the same coldkey-hotkey pair ownership.
628+
Like subnet hopping - same owner, same hotkey, just changing which subnet the stake is in.
629+
630+
Both hotkey and coldkey are the same.
631+
632+
Args:
633+
wallet (bittensor.wallet): The wallet to transfer stake from.
634+
hotkey_ss58 (str): The SS58 address of the hotkey whose stake is being swapped.
635+
origin_netuid (int): The netuid from which stake is removed.
636+
destination_netuid (int): The netuid to which stake is added.
637+
amount (Union[Balance, float, int]): The amount to swap.
638+
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
639+
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
640+
641+
Returns:
642+
success (bool): True if the extrinsic was successful.
643+
"""
644+
if isinstance(amount, (float, int)):
645+
amount = Balance.from_tao(amount)
646+
647+
hotkey_owner = await self.get_hotkey_owner(hotkey_ss58)
648+
if hotkey_owner != wallet.coldkeypub.ss58_address:
649+
logging.error(
650+
f":cross_mark: [red]Failed[/red]: Hotkey: {hotkey_ss58} does not belong to the origin coldkey owner: {wallet.coldkeypub.ss58_address}"
651+
)
652+
return False
653+
654+
stake_in_origin = await self.get_stake(
655+
hotkey_ss58=hotkey_ss58,
656+
coldkey_ss58=wallet.coldkeypub.ss58_address,
657+
netuid=origin_netuid,
658+
)
659+
if stake_in_origin < amount:
660+
logging.error(
661+
f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}"
662+
)
663+
return False
664+
665+
call = await self.substrate.compose_call(
666+
call_module="SubtensorModule",
667+
call_function="swap_stake",
668+
call_params={
669+
"hotkey": hotkey_ss58,
670+
"origin_netuid": origin_netuid,
671+
"destination_netuid": destination_netuid,
672+
"alpha_amount": amount.rao,
673+
},
674+
)
675+
next_nonce = await self.substrate.get_account_next_index(
676+
wallet.coldkeypub.ss58_address
677+
)
678+
extrinsic = await self.substrate.create_signed_extrinsic(
679+
call=call,
680+
keypair=wallet.coldkey,
681+
nonce=next_nonce,
682+
)
683+
response = await self.substrate.submit_extrinsic(
684+
extrinsic,
685+
wait_for_inclusion=wait_for_inclusion,
686+
wait_for_finalization=wait_for_finalization,
687+
)
688+
if not wait_for_finalization and not wait_for_inclusion:
689+
return True
690+
691+
if await response.is_success:
692+
return True
693+
else:
694+
logging.error(
695+
f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}"
696+
)
697+
return False
698+
699+
async def move_stake(
700+
self,
701+
wallet: "Wallet",
702+
origin_hotkey: str,
703+
origin_netuid: int,
704+
destination_hotkey: str,
705+
destination_netuid: int,
706+
amount: Union["Balance", float, int],
707+
wait_for_inclusion: bool = True,
708+
wait_for_finalization: bool = False,
709+
) -> bool:
710+
"""
711+
Moves stake to a different hotkey and/or subnet while keeping the same coldkey owner.
712+
Flexible movement allowing changes to both hotkey and subnet under the same coldkey's control.
713+
714+
Coldkey is the same. Hotkeys can be different.
715+
716+
Args:
717+
wallet (bittensor.wallet): The wallet to transfer stake from.
718+
origin_hotkey (str): The SS58 address of the source hotkey.
719+
origin_netuid (int): The netuid of the source subnet.
720+
destination_hotkey (str): The SS58 address of the destination hotkey.
721+
destination_netuid (int): The netuid of the destination subnet.
722+
amount (Union[Balance, float, int]): Amount of stake to move.
723+
wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is True.
724+
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is False.
725+
726+
Returns:
727+
bool: True if the stake movement was successful, False otherwise.
728+
729+
Raises:
730+
StakeError: If the movement fails due to insufficient stake or other reasons.
731+
"""
732+
if isinstance(amount, (float, int)):
733+
amount = Balance.from_tao(amount)
734+
735+
origin_owner = await self.get_hotkey_owner(origin_hotkey)
736+
if origin_owner != wallet.coldkeypub.ss58_address:
737+
logging.error(
738+
f":cross_mark: [red]Failed[/red]: Origin hotkey: {origin_hotkey} does not belong to the coldkey owner: {wallet.coldkeypub.ss58_address}"
739+
)
740+
return False
741+
742+
stake_in_origin = await self.get_stake(
743+
hotkey_ss58=origin_hotkey,
744+
coldkey_ss58=wallet.coldkeypub.ss58_address,
745+
netuid=origin_netuid,
746+
)
747+
if stake_in_origin < amount:
748+
logging.error(
749+
f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {origin_hotkey}. Stake: {stake_in_origin}, amount: {amount}"
750+
)
751+
return False
752+
753+
call = await self.substrate.compose_call(
754+
call_module="SubtensorModule",
755+
call_function="move_stake",
756+
call_params={
757+
"origin_hotkey": origin_hotkey,
758+
"origin_netuid": origin_netuid,
759+
"destination_hotkey": destination_hotkey,
760+
"destination_netuid": destination_netuid,
761+
"alpha_amount": amount.rao,
762+
},
763+
)
764+
765+
next_nonce = await self.substrate.get_account_next_index(
766+
wallet.coldkeypub.ss58_address
767+
)
768+
extrinsic = await self.substrate.create_signed_extrinsic(
769+
call=call,
770+
keypair=wallet.coldkey,
771+
nonce=next_nonce,
772+
)
773+
774+
response = await self.substrate.submit_extrinsic(
775+
extrinsic,
776+
wait_for_inclusion=wait_for_inclusion,
777+
wait_for_finalization=wait_for_finalization,
778+
)
779+
780+
if not wait_for_finalization and not wait_for_inclusion:
781+
return True
782+
783+
if await response.is_success:
784+
return True
785+
else:
786+
logging.error(
787+
f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}"
788+
)
789+
return False
790+
529791
async def is_hotkey_registered_any(
530792
self,
531793
hotkey_ss58: str,

bittensor/core/extrinsics/transfer.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from typing import Optional, Union, TYPE_CHECKING
1919

2020
from bittensor.core.extrinsics.utils import submit_extrinsic
21+
from bittensor.core.errors import StakeError
2122
from bittensor.core.settings import NETWORK_EXPLORER_MAP
2223
from bittensor.utils import (
2324
get_explorer_url_for_network,
@@ -197,3 +198,106 @@ def transfer_extrinsic(
197198
return True
198199

199200
return False
201+
202+
203+
def transfer_stake_extrinsic(
204+
subtensor: "Subtensor",
205+
wallet: "Wallet",
206+
hotkey_ss58: str,
207+
amount: Optional[Union[Balance, float, int]],
208+
origin_netuid: int,
209+
destination_netuid: int,
210+
destination_coldkey_ss58: str,
211+
wait_for_inclusion: bool = True,
212+
wait_for_finalization: bool = False,
213+
) -> bool:
214+
"""Transfers stake from one network to another.
215+
216+
Args:
217+
subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance.
218+
wallet (Wallet): Bittensor wallet object.
219+
hotkey_ss58 (str): The ``ss58`` address of the hotkey account to transfer stake from.
220+
amount (Union[Balance, float, int]): Amount to transfer as Bittensor balance, float or int.
221+
origin_netuid (int): The netuid to transfer stake from.
222+
destination_netuid (int): The netuid to transfer stake to.
223+
destination_coldkey_ss58 (str): The destination coldkey to transfer stake to.
224+
wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning.
225+
wait_for_finalization (bool): If set, waits for the extrinsic to be finalized before returning.
226+
227+
Returns:
228+
success (bool): True if the transfer was successful.
229+
"""
230+
# Decrypt keys
231+
if not (unlock := unlock_key(wallet)).success:
232+
logging.error(unlock.message)
233+
return False
234+
235+
if not isinstance(amount, Balance):
236+
amount = Balance.from_tao(amount).set_unit(origin_netuid)
237+
238+
logging.info(
239+
f":satellite: [magenta]Transferring stake on:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]"
240+
)
241+
242+
old_stake = subtensor.get_stake_for_coldkey_and_hotkey(
243+
coldkey_ss58=wallet.coldkeypub.ss58_address,
244+
hotkey_ss58=hotkey_ss58,
245+
netuid=origin_netuid,
246+
)
247+
if old_stake < amount:
248+
logging.error(
249+
f":cross_mark: [red]Failed[/red]: Not enough stake on netuid {origin_netuid} to transfer. Stake: {old_stake} < Amount: {amount}"
250+
)
251+
return False
252+
try:
253+
logging.info(
254+
f":satellite: [magenta]Transferring:[/magenta] [blue]{amount} from netuid: {origin_netuid} to netuid: {destination_netuid}[/blue]"
255+
)
256+
257+
call = subtensor.substrate.compose_call(
258+
call_module="SubtensorModule",
259+
call_function="transfer_stake",
260+
call_params={
261+
"destination_coldkey": destination_coldkey_ss58,
262+
"hotkey": hotkey_ss58,
263+
"origin_netuid": origin_netuid,
264+
"destination_netuid": destination_netuid,
265+
"alpha_amount": amount.rao,
266+
},
267+
)
268+
extrinsic = subtensor.substrate.create_signed_extrinsic(
269+
call=call, keypair=wallet.coldkey
270+
)
271+
response = subtensor.substrate.submit_extrinsic(
272+
extrinsic,
273+
wait_for_inclusion=wait_for_inclusion,
274+
wait_for_finalization=wait_for_finalization,
275+
)
276+
277+
if not wait_for_finalization and not wait_for_inclusion:
278+
return True
279+
280+
response.process_events()
281+
if response.is_success:
282+
logging.success(":white_heavy_check_mark: [green]Finalized[/green]")
283+
284+
# Get new stake
285+
new_stake = subtensor.get_stake_for_coldkey_and_hotkey(
286+
coldkey_ss58=wallet.coldkeypub.ss58_address,
287+
hotkey_ss58=hotkey_ss58,
288+
netuid=origin_netuid,
289+
)
290+
291+
logging.info(
292+
f"Origin Stake: [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]"
293+
)
294+
return True
295+
else:
296+
logging.error(
297+
f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}"
298+
)
299+
return False
300+
301+
except StakeError as e:
302+
logging.error(f":cross_mark: [red]Transfer Stake Error: {e}[/red]")
303+
return False

0 commit comments

Comments
 (0)