From aa1cf1132368d27dcca30cd5448a376a8da0b87a Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Jul 2025 22:28:42 +0200 Subject: [PATCH 1/9] Transfers. --- bittensor/core/async_subtensor.py | 7 ++-- bittensor/core/extrinsics/asyncex/transfer.py | 38 ++++++++++++++----- bittensor/core/extrinsics/transfer.py | 38 ++++++++++++++----- bittensor/core/subtensor.py | 5 ++- 4 files changed, 64 insertions(+), 24 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index c78f28f165..caeb837a2a 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -5455,7 +5455,7 @@ async def transfer( self, wallet: "Wallet", dest: str, - amount: Balance, + amount: Optional[Balance], transfer_all: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -5468,7 +5468,7 @@ async def transfer( Arguments: wallet: Source wallet for the transfer. dest: Destination address for the transfer. - amount: Number of tokens to transfer. + amount: Number of tokens to transfer. `None` is transferring all. transfer_all: Flag to transfer all tokens. Default is `False`. wait_for_inclusion: Waits for the transaction to be included in a block. Defaults to `True`. wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Defaults to `False`. @@ -5479,7 +5479,8 @@ async def transfer( Returns: `True` if the transferring was successful, otherwise `False`. """ - amount = check_and_convert_to_balance(amount) + if amount is not None: + amount = check_and_convert_to_balance(amount) return await transfer_extrinsic( subtensor=self, wallet=wallet, diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index e98e234307..e78c784678 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -19,7 +19,8 @@ async def _do_transfer( subtensor: "AsyncSubtensor", wallet: "Wallet", destination: str, - amount: "Balance", + amount: Optional[Balance], + keep_alive: bool = True, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, @@ -32,6 +33,7 @@ async def _do_transfer( wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. destination (str): Destination public key address (ss58_address or ed25519) of recipient. amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. + keep_alive (bool): If `True`, will keep the existential deposit in the account. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning @@ -43,10 +45,24 @@ async def _do_transfer( Returns: success, block hash, formatted error message """ + call_params = {"dest": destination} + if amount is None: + call_function = "transfer_all" + if keep_alive: + call_params["keep_alive"] = True + else: + call_params["keep_alive"] = False + else: + call_params["amount"] = amount.rao + if keep_alive: + call_function = "transfer_keep_alive" + else: + call_function = "transfer_allow_death" + call = await subtensor.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": destination, "value": amount.rao}, + call_function=call_function, + call_params=call_params, ) success, message = await subtensor.sign_and_send_extrinsic( @@ -73,7 +89,7 @@ async def transfer_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", dest: str, - amount: "Balance", + amount: Optional[Balance], transfer_all: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -86,7 +102,8 @@ async def transfer_extrinsic( subtensor (bittensor.core.async_subtensor.AsyncSubtensor): initialized AsyncSubtensor object used for transfer wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. dest (str): Destination public key address (ss58_address or ed25519) of recipient. - amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. + amount (Optional[bittensor.utils.balance.Balance]): Amount to stake as Bittensor balance. `None` if + transferring all. transfer_all (bool): Whether to transfer all funds from this wallet to the destination address. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. @@ -102,6 +119,11 @@ async def transfer_extrinsic( finalization / inclusion, the response is `True`, regardless of its inclusion. """ destination = dest + + if amount is None and not transfer_all: + logging.error("If not transferring all, `amount` must be specified.") + return False + # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): logging.error( @@ -137,12 +159,10 @@ async def transfer_extrinsic( # Check if we have enough balance. if transfer_all is True: - amount = account_balance - fee - existential_deposit - if amount < Balance(0): + if (account_balance - fee) < existential_deposit: logging.error("Not enough balance to transfer") return False - - if account_balance < (amount + fee + existential_deposit): + elif account_balance < (amount + fee + existential_deposit): logging.error(":cross_mark: [red]Not enough balance[/red]") logging.error(f"\t\tBalance:\t[blue]{account_balance}[/blue]") logging.error(f"\t\tAmount:\t[blue]{amount}[/blue]") diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 370efaed36..1a0c28a76d 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -18,7 +18,8 @@ def _do_transfer( subtensor: "Subtensor", wallet: "Wallet", destination: str, - amount: Balance, + amount: Optional[Balance], + keep_alive: bool = True, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, @@ -30,7 +31,8 @@ def _do_transfer( subtensor (bittensor.core.subtensor.Subtensor): the Subtensor object used for transfer wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. destination (str): Destination public key address (ss58_address or ed25519) of recipient. - amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. + amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. `None` if transferring all. + keep_alive (bool): If `True`, will keep the existential deposit in the account. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning @@ -42,10 +44,24 @@ def _do_transfer( Returns: success, block hash, formatted error message """ + call_params = {"dest": destination} + if amount is None: + call_function = "transfer_all" + if keep_alive: + call_params["keep_alive"] = True + else: + call_params["keep_alive"] = False + else: + call_params["amount"] = amount.rao + if keep_alive: + call_function = "transfer_keep_alive" + else: + call_function = "transfer_allow_death" + call = subtensor.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": destination, "value": amount.rao}, + call_function=call_function, + call_params=call_params, ) success, message = subtensor.sign_and_send_extrinsic( @@ -72,7 +88,7 @@ def transfer_extrinsic( subtensor: "Subtensor", wallet: "Wallet", dest: str, - amount: Balance, + amount: Optional[Balance], transfer_all: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -85,7 +101,7 @@ def transfer_extrinsic( subtensor (bittensor.core.subtensor.Subtensor): the Subtensor object used for transfer wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. dest (str): Destination public key address (ss58_address or ed25519) of recipient. - amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. + amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. `None` if transferring all. transfer_all (bool): Whether to transfer all funds from this wallet to the destination address. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. @@ -100,6 +116,10 @@ def transfer_extrinsic( success (bool): Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is `True`, regardless of its inclusion. """ + if amount is None and not transfer_all: + logging.error("If not transferring all, `amount` must be specified.") + return False + # Validate destination address. if not is_valid_bittensor_address_or_public_key(dest): logging.error( @@ -131,12 +151,10 @@ def transfer_extrinsic( # Check if we have enough balance. if transfer_all is True: - amount = account_balance - fee - existential_deposit - if amount < Balance(0): + if (account_balance - fee) < existential_deposit: logging.error("Not enough balance to transfer") return False - - if account_balance < (amount + fee + existential_deposit): + elif account_balance < (amount + fee + existential_deposit): logging.error(":cross_mark: [red]Not enough balance[/red]") logging.error(f"\t\tBalance:\t[blue]{account_balance}[/blue]") logging.error(f"\t\tAmount:\t[blue]{amount}[/blue]") diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index b28c49533f..10f0963882 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4266,7 +4266,7 @@ def transfer( self, wallet: "Wallet", dest: str, - amount: Balance, + amount: Optional[Balance], wait_for_inclusion: bool = True, wait_for_finalization: bool = False, transfer_all: bool = False, @@ -4292,7 +4292,8 @@ def transfer( Returns: `True` if the transferring was successful, otherwise `False`. """ - amount = check_and_convert_to_balance(amount) + if amount is not None: + amount = check_and_convert_to_balance(amount) return transfer_extrinsic( subtensor=self, wallet=wallet, From c27082e0fe1698d32c33583c412f16a44f829f6f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Jul 2025 22:31:26 +0200 Subject: [PATCH 2/9] Missed kwarg --- bittensor/core/extrinsics/asyncex/transfer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index e78c784678..c5fd8271ce 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -175,6 +175,7 @@ async def transfer_extrinsic( wallet=wallet, destination=destination, amount=amount, + keep_alive=keep_alive, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, period=period, From c60c4861ee45d74d657caceea69059df73777d18 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Jul 2025 22:32:07 +0200 Subject: [PATCH 3/9] Missed kwarg --- bittensor/core/extrinsics/transfer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 1a0c28a76d..25bf1b554b 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -167,6 +167,7 @@ def transfer_extrinsic( wallet=wallet, destination=dest, amount=amount, + keep_alive=keep_alive, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, period=period, From e6e987f184f7d2c4e81c3523759e678f6484e499 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Jul 2025 22:32:59 +0200 Subject: [PATCH 4/9] Oepsie --- bittensor/core/extrinsics/transfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 25bf1b554b..c0f335d95b 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -52,7 +52,7 @@ def _do_transfer( else: call_params["keep_alive"] = False else: - call_params["amount"] = amount.rao + call_params["value"] = amount.rao if keep_alive: call_function = "transfer_keep_alive" else: From c81c0304c3b39ba87171e92bd2f09aa95463fef9 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Jul 2025 22:46:35 +0200 Subject: [PATCH 5/9] Tests --- bittensor/core/extrinsics/asyncex/transfer.py | 2 +- .../extrinsics/asyncex/test_transfer.py | 33 +++++++++++++--- tests/unit_tests/extrinsics/test_transfer.py | 39 ++++++++++++++++--- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index c5fd8271ce..bae1184811 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -53,7 +53,7 @@ async def _do_transfer( else: call_params["keep_alive"] = False else: - call_params["amount"] = amount.rao + call_params["value"] = amount.rao if keep_alive: call_function = "transfer_keep_alive" else: diff --git a/tests/unit_tests/extrinsics/asyncex/test_transfer.py b/tests/unit_tests/extrinsics/asyncex/test_transfer.py index 95c5249b62..299c6df446 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_transfer.py +++ b/tests/unit_tests/extrinsics/asyncex/test_transfer.py @@ -3,12 +3,32 @@ from bittensor.utils.balance import Balance +@pytest.mark.parametrize( + "amount,keep_alive,call_function,call_params", + [ + ( + Balance(1), + True, + "transfer_keep_alive", + {"dest": "SS58PUBLICKEY", "value": Balance(1).rao}, + ), + (None, True, "transfer_all", {"dest": "SS58PUBLICKEY", "keep_alive": True}), + (None, False, "transfer_all", {"dest": "SS58PUBLICKEY", "keep_alive": False}), + ( + Balance(1), + False, + "transfer_allow_death", + {"dest": "SS58PUBLICKEY", "value": Balance(1).rao}, + ), + ], +) @pytest.mark.asyncio -async def test_do_transfer_success(subtensor, fake_wallet, mocker): +async def test_do_transfer_success( + subtensor, fake_wallet, mocker, amount, keep_alive, call_function, call_params +): """Tests _do_transfer when the transfer is successful.""" # Preps - fake_destination = "destination_address" - fake_amount = mocker.Mock(autospec=Balance, rao=1000) + fake_destination = "SS58PUBLICKEY" fake_block_hash = "fake_block_hash" mocker.patch.object(subtensor.substrate, "compose_call") @@ -24,7 +44,8 @@ async def test_do_transfer_success(subtensor, fake_wallet, mocker): subtensor=subtensor, wallet=fake_wallet, destination=fake_destination, - amount=fake_amount, + amount=amount, + keep_alive=keep_alive, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -32,8 +53,8 @@ async def test_do_transfer_success(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_awaited_once_with( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": fake_destination, "value": fake_amount.rao}, + call_function=call_function, + call_params=call_params, ) subtensor.sign_and_send_extrinsic.assert_awaited_once_with( diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index 081a56ffae..458bc9bd46 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -1,12 +1,34 @@ from bittensor.core.extrinsics.transfer import _do_transfer from bittensor.utils.balance import Balance - -def test_do_transfer_is_success_true(subtensor, fake_wallet, mocker): +import pytest + + +@pytest.mark.parametrize( + "amount,keep_alive,call_function,call_params", + [ + ( + Balance(1), + True, + "transfer_keep_alive", + {"dest": "SS58PUBLICKEY", "value": Balance(1).rao}, + ), + (None, True, "transfer_all", {"dest": "SS58PUBLICKEY", "keep_alive": True}), + (None, False, "transfer_all", {"dest": "SS58PUBLICKEY", "keep_alive": False}), + ( + Balance(1), + False, + "transfer_allow_death", + {"dest": "SS58PUBLICKEY", "value": Balance(1).rao}, + ), + ], +) +def test_do_transfer_is_success_true( + subtensor, fake_wallet, mocker, amount, keep_alive, call_function, call_params +): """Successful do_transfer call.""" # Prep fake_dest = "SS58PUBLICKEY" - fake_transfer_balance = Balance(1) fake_wait_for_inclusion = True fake_wait_for_finalization = True @@ -18,7 +40,8 @@ def test_do_transfer_is_success_true(subtensor, fake_wallet, mocker): subtensor, fake_wallet, fake_dest, - fake_transfer_balance, + amount, + keep_alive, fake_wait_for_inclusion, fake_wait_for_finalization, ) @@ -26,8 +49,8 @@ def test_do_transfer_is_success_true(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, + call_function=call_function, + call_params=call_params, ) subtensor.sign_and_send_extrinsic.assert_called_once_with( call=subtensor.substrate.compose_call.return_value, @@ -45,6 +68,7 @@ def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): # Prep fake_dest = "SS58PUBLICKEY" fake_transfer_balance = Balance(1) + keep_alive = True fake_wait_for_inclusion = True fake_wait_for_finalization = True @@ -57,6 +81,7 @@ def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): fake_wallet, fake_dest, fake_transfer_balance, + keep_alive, fake_wait_for_inclusion, fake_wait_for_finalization, ) @@ -83,6 +108,7 @@ def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): # Prep fake_dest = "SS58PUBLICKEY" fake_transfer_balance = Balance(1) + keep_alive = True fake_wait_for_inclusion = False fake_wait_for_finalization = False @@ -96,6 +122,7 @@ def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): fake_wallet, fake_dest, fake_transfer_balance, + keep_alive, fake_wait_for_inclusion, fake_wait_for_finalization, ) From b3eba69018b3aa7d2b37da2d71865e8e767dbcda Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Jul 2025 14:41:37 +0200 Subject: [PATCH 6/9] Updated arg order --- bittensor/core/extrinsics/asyncex/transfer.py | 4 ++-- bittensor/core/extrinsics/transfer.py | 4 ++-- tests/unit_tests/extrinsics/test_transfer.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index bae1184811..69f23c3675 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -20,10 +20,10 @@ async def _do_transfer( wallet: "Wallet", destination: str, amount: Optional[Balance], - keep_alive: bool = True, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + keep_alive: bool = True, ) -> tuple[bool, str, str]: """ Makes transfer from wallet to destination public key address. @@ -33,7 +33,6 @@ async def _do_transfer( wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. destination (str): Destination public key address (ss58_address or ed25519) of recipient. amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. - keep_alive (bool): If `True`, will keep the existential deposit in the account. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning @@ -41,6 +40,7 @@ async def _do_transfer( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + keep_alive (bool): If `True`, will keep the existential deposit in the account. Returns: success, block hash, formatted error message diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index c0f335d95b..6dd8e8ac33 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -19,10 +19,10 @@ def _do_transfer( wallet: "Wallet", destination: str, amount: Optional[Balance], - keep_alive: bool = True, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + keep_alive: bool = True, ) -> tuple[bool, str, str]: """ Makes transfer from wallet to destination public key address. @@ -32,7 +32,6 @@ def _do_transfer( wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. destination (str): Destination public key address (ss58_address or ed25519) of recipient. amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. `None` if transferring all. - keep_alive (bool): If `True`, will keep the existential deposit in the account. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning @@ -40,6 +39,7 @@ def _do_transfer( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + keep_alive (bool): If `True`, will keep the existential deposit in the account. Returns: success, block hash, formatted error message diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index 458bc9bd46..6aedaea601 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -41,9 +41,9 @@ def test_do_transfer_is_success_true( fake_wallet, fake_dest, amount, - keep_alive, fake_wait_for_inclusion, fake_wait_for_finalization, + keep_alive=keep_alive, ) # Asserts @@ -81,9 +81,9 @@ def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): fake_wallet, fake_dest, fake_transfer_balance, - keep_alive, fake_wait_for_inclusion, fake_wait_for_finalization, + keep_alive=keep_alive, ) # Asserts @@ -122,9 +122,9 @@ def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): fake_wallet, fake_dest, fake_transfer_balance, - keep_alive, fake_wait_for_inclusion, fake_wait_for_finalization, + keep_alive=keep_alive, ) # Asserts From 0e93f2ab49441588bb1dc6c0053decf4f3d4e47f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Jul 2025 15:31:05 +0200 Subject: [PATCH 7/9] Added e2e tests --- bittensor/core/async_subtensor.py | 23 ++++- bittensor/core/extrinsics/asyncex/transfer.py | 2 +- bittensor/core/extrinsics/transfer.py | 4 +- bittensor/core/subtensor.py | 29 +++++- tests/e2e_tests/test_transfer.py | 97 ++++++++++++++++++- 5 files changed, 144 insertions(+), 11 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index caeb837a2a..286d41230e 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3134,7 +3134,7 @@ async def get_total_subnets( return getattr(result, "value", None) async def get_transfer_fee( - self, wallet: "Wallet", dest: str, value: Balance + self, wallet: "Wallet", dest: str, value: Balance, keep_alive: bool = True ) -> Balance: """ Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This @@ -3146,6 +3146,8 @@ async def get_transfer_fee( dest: The ``SS58`` address of the destination account. value: The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. + keep_alive: Whether the transfer fee should be calculated based on keeping the wallet alive (existential + deposit) or not. Returns: bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance @@ -3155,12 +3157,25 @@ async def get_transfer_fee( wallet has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ - value = check_and_convert_to_balance(value) + call_params = {"dest": dest} + if value is None: + call_function = "transfer_all" + if keep_alive: + call_params["keep_alive"] = True + else: + call_params["keep_alive"] = False + else: + value = check_and_convert_to_balance(value) + call_params["value"] = value.rao + if keep_alive: + call_function = "transfer_keep_alive" + else: + call_function = "transfer_allow_death" call = await self.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": dest, "value": value.rao}, + call_function=call_function, + call_params=call_params, ) try: diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index 69f23c3675..f5b0a06b41 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -150,7 +150,7 @@ async def transfer_extrinsic( ) fee = await subtensor.get_transfer_fee( - wallet=wallet, dest=destination, value=amount + wallet=wallet, dest=destination, value=amount, keep_alive=keep_alive ) if not keep_alive: diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 6dd8e8ac33..fec38b1ffc 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -147,7 +147,9 @@ def transfer_extrinsic( else: existential_deposit = subtensor.get_existential_deposit(block=block) - fee = subtensor.get_transfer_fee(wallet=wallet, dest=dest, value=amount) + fee = subtensor.get_transfer_fee( + wallet=wallet, dest=dest, value=amount, keep_alive=keep_alive + ) # Check if we have enough balance. if transfer_all is True: diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 10f0963882..e628f057b2 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2243,7 +2243,13 @@ def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: ) return getattr(result, "value", None) - def get_transfer_fee(self, wallet: "Wallet", dest: str, value: Balance) -> Balance: + def get_transfer_fee( + self, + wallet: "Wallet", + dest: str, + value: Optional[Balance], + keep_alive: bool = True, + ) -> Balance: """ Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This function simulates the transfer to estimate the associated cost, taking into account the current network @@ -2254,6 +2260,8 @@ def get_transfer_fee(self, wallet: "Wallet", dest: str, value: Balance) -> Balan dest (str): The ``SS58`` address of the destination account. value (Union[bittensor.utils.balance.Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. + keep_alive: Whether the transfer fee should be calculated based on keeping the wallet alive (existential + deposit) or not. Returns: bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance @@ -2263,11 +2271,24 @@ def get_transfer_fee(self, wallet: "Wallet", dest: str, value: Balance) -> Balan has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ - value = check_and_convert_to_balance(value) + call_params = {"dest": dest} + if value is None: + call_function = "transfer_all" + if keep_alive: + call_params["keep_alive"] = True + else: + call_params["keep_alive"] = False + else: + value = check_and_convert_to_balance(value) + call_params["value"] = value.rao + if keep_alive: + call_function = "transfer_keep_alive" + else: + call_function = "transfer_allow_death" call = self.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": dest, "value": value.rao}, + call_function=call_function, + call_params=call_params, ) try: diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py index 7a0728de72..0663b540b2 100644 --- a/tests/e2e_tests/test_transfer.py +++ b/tests/e2e_tests/test_transfer.py @@ -1,10 +1,18 @@ +import typing + +from bittensor_wallet import Wallet +import pytest + from bittensor.utils.balance import Balance from bittensor import logging +if typing.TYPE_CHECKING: + from bittensor.core.subtensor_api import SubtensorApi + logging.set_trace() -def test_transfer(subtensor, alice_wallet): +def test_transfer(subtensor: "SubtensorApi", alice_wallet): """ Test the transfer mechanism on the chain @@ -47,3 +55,90 @@ def test_transfer(subtensor, alice_wallet): ) print("✅ Passed test_transfer") + + +def test_transfer_all(subtensor: "Subtensor", alice_wallet): + # create two dummy accounts we can drain + dummy_account_1 = Wallet(path="/tmp/bittensor-dummy-account-1") + dummy_account_2 = Wallet(path="/tmp/bittensor-dummy-account-2") + dummy_account_1.create_new_coldkey(use_password=False, overwrite=True) + dummy_account_2.create_new_coldkey(use_password=False, overwrite=True) + + # fund the first dummy account + assert subtensor.transfer( + alice_wallet, + dest=dummy_account_1.coldkeypub.ss58_address, + amount=Balance.from_tao(2.0), + wait_for_finalization=True, + wait_for_inclusion=True, + ) + # Account details before transfer + existential_deposit = subtensor.get_existential_deposit() + assert subtensor.transfer( + wallet=dummy_account_1, + dest=dummy_account_2.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_finalization=True, + wait_for_inclusion=True, + keep_alive=True, + ) + balance_after = subtensor.get_balance(dummy_account_1.coldkeypub.ss58_address) + assert balance_after == existential_deposit + assert subtensor.transfer( + wallet=dummy_account_2, + dest=alice_wallet.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_inclusion=True, + wait_for_finalization=True, + keep_alive=False, + ) + balance_after = subtensor.get_balance(dummy_account_2.coldkeypub.ss58_address) + assert balance_after == Balance(0) + + +@pytest.mark.asyncio +async def test_async_transfer(async_subtensor: "SubtensorApi", alice_wallet): + # create two dummy accounts we can drain + dummy_account_1 = Wallet(path="/tmp/bittensor-dummy-account-3") + dummy_account_2 = Wallet(path="/tmp/bittensor-dummy-account-4") + dummy_account_1.create_new_coldkey(use_password=False, overwrite=True) + dummy_account_2.create_new_coldkey(use_password=False, overwrite=True) + + # fund the first dummy account + assert await async_subtensor.transfer( + alice_wallet, + dest=dummy_account_1.coldkeypub.ss58_address, + amount=Balance.from_tao(2.0), + wait_for_finalization=True, + wait_for_inclusion=True, + ) + # Account details before transfer + existential_deposit = await async_subtensor.get_existential_deposit() + assert await async_subtensor.transfer( + wallet=dummy_account_1, + dest=dummy_account_2.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_finalization=True, + wait_for_inclusion=True, + keep_alive=True, + ) + balance_after = await async_subtensor.get_balance( + dummy_account_1.coldkeypub.ss58_address + ) + assert balance_after == existential_deposit + assert await async_subtensor.transfer( + wallet=dummy_account_2, + dest=alice_wallet.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_inclusion=True, + wait_for_finalization=True, + keep_alive=False, + ) + balance_after = await async_subtensor.get_balance( + dummy_account_2.coldkeypub.ss58_address + ) + assert balance_after == Balance(0) From f404ea5780321fd5bfed637dd6ace0dbdaae441c Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Jul 2025 15:36:15 +0200 Subject: [PATCH 8/9] Mypy --- bittensor/core/async_subtensor.py | 2 +- bittensor/core/subtensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 286d41230e..474ca1ac10 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3157,7 +3157,7 @@ async def get_transfer_fee( wallet has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ - call_params = {"dest": dest} + call_params: dict[str, Union[int, str, bool]] = {"dest": dest} if value is None: call_function = "transfer_all" if keep_alive: diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index e628f057b2..e1a4d8b666 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2271,7 +2271,7 @@ def get_transfer_fee( has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ - call_params = {"dest": dest} + call_params: dict[str, Union[int, str, bool]] = {"dest": dest} if value is None: call_function = "transfer_all" if keep_alive: From 10ab0d5c8eb8395e22ed371e4dcd5fa0a2fc19a9 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Jul 2025 18:23:41 +0200 Subject: [PATCH 9/9] Helper function --- bittensor/core/async_subtensor.py | 17 +++------- bittensor/core/extrinsics/asyncex/transfer.py | 15 ++------- bittensor/core/extrinsics/transfer.py | 15 ++------- bittensor/core/subtensor.py | 18 +++-------- bittensor/utils/__init__.py | 32 +++++++++++++++++++ 5 files changed, 45 insertions(+), 52 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 474ca1ac10..98542c89a6 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -98,6 +98,7 @@ torch, u16_normalized_float, u64_normalized_float, + get_transfer_fn_params, ) from bittensor.core.extrinsics.asyncex.liquidity import ( add_liquidity_extrinsic, @@ -3157,20 +3158,10 @@ async def get_transfer_fee( wallet has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ - call_params: dict[str, Union[int, str, bool]] = {"dest": dest} - if value is None: - call_function = "transfer_all" - if keep_alive: - call_params["keep_alive"] = True - else: - call_params["keep_alive"] = False - else: + if value is not None: value = check_and_convert_to_balance(value) - call_params["value"] = value.rao - if keep_alive: - call_function = "transfer_keep_alive" - else: - call_function = "transfer_allow_death" + call_params: dict[str, Union[int, str, bool]] + call_function, call_params = get_transfer_fn_params(value, dest, keep_alive) call = await self.substrate.compose_call( call_module="Balances", diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index f5b0a06b41..6b0332cd73 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -6,6 +6,7 @@ get_explorer_url_for_network, is_valid_bittensor_address_or_public_key, unlock_key, + get_transfer_fn_params, ) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -45,19 +46,7 @@ async def _do_transfer( Returns: success, block hash, formatted error message """ - call_params = {"dest": destination} - if amount is None: - call_function = "transfer_all" - if keep_alive: - call_params["keep_alive"] = True - else: - call_params["keep_alive"] = False - else: - call_params["value"] = amount.rao - if keep_alive: - call_function = "transfer_keep_alive" - else: - call_function = "transfer_allow_death" + call_function, call_params = get_transfer_fn_params(amount, destination, keep_alive) call = await subtensor.substrate.compose_call( call_module="Balances", diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index fec38b1ffc..9faecdcea4 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -5,6 +5,7 @@ is_valid_bittensor_address_or_public_key, unlock_key, get_explorer_url_for_network, + get_transfer_fn_params, ) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -44,19 +45,7 @@ def _do_transfer( Returns: success, block hash, formatted error message """ - call_params = {"dest": destination} - if amount is None: - call_function = "transfer_all" - if keep_alive: - call_params["keep_alive"] = True - else: - call_params["keep_alive"] = False - else: - call_params["value"] = amount.rao - if keep_alive: - call_function = "transfer_keep_alive" - else: - call_function = "transfer_allow_death" + call_function, call_params = get_transfer_fn_params(amount, destination, keep_alive) call = subtensor.substrate.compose_call( call_module="Balances", diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index e1a4d8b666..d1b95521d8 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -107,6 +107,7 @@ u16_normalized_float, u64_normalized_float, deprecated_message, + get_transfer_fn_params, ) from bittensor.utils.balance import ( Balance, @@ -2271,20 +2272,11 @@ def get_transfer_fee( has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ - call_params: dict[str, Union[int, str, bool]] = {"dest": dest} - if value is None: - call_function = "transfer_all" - if keep_alive: - call_params["keep_alive"] = True - else: - call_params["keep_alive"] = False - else: + if value is not None: value = check_and_convert_to_balance(value) - call_params["value"] = value.rao - if keep_alive: - call_function = "transfer_keep_alive" - else: - call_function = "transfer_allow_death" + call_params: dict[str, Union[int, str, bool]] + call_function, call_params = get_transfer_fn_params(value, dest, keep_alive) + call = self.substrate.compose_call( call_module="Balances", call_function=call_function, diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 26da8b5480..0d808477be 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: from bittensor_wallet import Wallet + from bittensor.utils.balance import Balance BT_DOCS_LINK = "https://docs.bittensor.com" @@ -445,3 +446,34 @@ def deprecated_message(message: str) -> None: """Shows a deprecation warning message with the given message.""" warnings.simplefilter("default", DeprecationWarning) warnings.warn(message=message, category=DeprecationWarning, stacklevel=2) + + +def get_transfer_fn_params( + amount: Optional["Balance"], destination: str, keep_alive: bool +) -> tuple[str, dict[str, Union[str, int, bool]]]: + """ + Helper function to get the transfer call function and call params, depending on the value and keep_alive flag + provided + + Args: + amount: the amount of Tao to transfer. `None` if transferring all. + destination: the destination SS58 of the transfer + keep_alive: whether to enforce a retention of the existential deposit in the account after transfer. + + Returns: + tuple[call function, call params] + """ + call_params = {"dest": destination} + if amount is None: + call_function = "transfer_all" + if keep_alive: + call_params["keep_alive"] = True + else: + call_params["keep_alive"] = False + else: + call_params["value"] = amount.rao + if keep_alive: + call_function = "transfer_keep_alive" + else: + call_function = "transfer_allow_death" + return call_function, call_params