@@ -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 ,
0 commit comments