diff --git a/bittensor/core/extrinsics/async_transfer.py b/bittensor/core/extrinsics/async_transfer.py
index e4190023d..814e23ae8 100644
--- a/bittensor/core/extrinsics/async_transfer.py
+++ b/bittensor/core/extrinsics/async_transfer.py
@@ -1,9 +1,6 @@
import asyncio
from typing import TYPE_CHECKING
-from bittensor_wallet import Wallet
-from substrateinterface.exceptions import SubstrateRequestException
-
from bittensor.core.settings import NETWORK_EXPLORER_MAP
from bittensor.utils import (
format_error_message,
@@ -16,11 +13,66 @@
if TYPE_CHECKING:
from bittensor.core.async_subtensor import AsyncSubtensor
+ from bittensor_wallet import Wallet
+
+
+async def _do_transfer(
+ subtensor: "AsyncSubtensor",
+ wallet: "Wallet",
+ destination: str,
+ amount: "Balance",
+ wait_for_inclusion: bool = True,
+ wait_for_finalization: bool = False,
+) -> tuple[bool, str, str]:
+ """
+ Makes transfer from wallet to destination public key address.
+
+ Args:
+ subtensor (bittensor.core.async_subtensor.AsyncSubtensor): initialized AsyncSubtensor 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.
+ 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 `True`, or returns `False` if the extrinsic fails to be finalized within the timeout.
+
+ Returns:
+ success, block hash, formatted error message
+ """
+ call = await subtensor.substrate.compose_call(
+ call_module="Balances",
+ call_function="transfer_allow_death",
+ call_params={"dest": destination, "value": amount.rao},
+ )
+ extrinsic = await subtensor.substrate.create_signed_extrinsic(
+ call=call, keypair=wallet.coldkey
+ )
+ response = await subtensor.substrate.submit_extrinsic(
+ extrinsic,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ )
+ # We only wait here if we expect finalization.
+ if not wait_for_finalization and not wait_for_inclusion:
+ return True, "", "Success, extrinsic submitted without waiting."
+
+ # Otherwise continue with finalization.
+ await response.process_events()
+ if await response.is_success:
+ block_hash_ = response.block_hash
+ return True, block_hash_, "Success with response."
+ else:
+ return (
+ False,
+ "",
+ format_error_message(
+ await response.error_message, substrate=subtensor.substrate
+ ),
+ )
async def transfer_extrinsic(
subtensor: "AsyncSubtensor",
- wallet: Wallet,
+ wallet: "Wallet",
destination: str,
amount: "Balance",
transfer_all: bool = False,
@@ -43,71 +95,6 @@ async def transfer_extrinsic(
Returns:
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.
"""
-
- async def get_transfer_fee() -> 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 conditions and transaction complexity.
- """
- call = await subtensor.substrate.compose_call(
- call_module="Balances",
- call_function="transfer_allow_death",
- call_params={"dest": destination, "value": amount.rao},
- )
-
- try:
- payment_info = await subtensor.substrate.get_payment_info(
- call=call, keypair=wallet.coldkeypub
- )
- except SubstrateRequestException as e:
- payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao
- logging.error(f":cross_mark: Failed to get payment info:")
- logging.error(f"\t\t{format_error_message(e, subtensor.substrate)}")
- logging.error(
- f"\t\tDefaulting to default transfer fee: {payment_info['partialFee']}"
- )
-
- return Balance.from_rao(payment_info["partialFee"])
-
- async def do_transfer() -> tuple[bool, str, str]:
- """
- Makes transfer from wallet to destination public key address.
-
- Returns:
- success, block hash, formatted error message
- """
- call = await subtensor.substrate.compose_call(
- call_module="Balances",
- call_function="transfer_allow_death",
- call_params={"dest": destination, "value": amount.rao},
- )
- extrinsic = await subtensor.substrate.create_signed_extrinsic(
- call=call, keypair=wallet.coldkey
- )
- response = await subtensor.substrate.submit_extrinsic(
- extrinsic,
- wait_for_inclusion=wait_for_inclusion,
- wait_for_finalization=wait_for_finalization,
- )
- # We only wait here if we expect finalization.
- if not wait_for_finalization and not wait_for_inclusion:
- return True, "", ""
-
- # Otherwise continue with finalization.
- await response.process_events()
- if await response.is_success:
- block_hash_ = response.block_hash
- return True, block_hash_, ""
- else:
- return (
- False,
- "",
- format_error_message(
- await response.error_message, substrate=subtensor.substrate
- ),
- )
-
# Validate destination address.
if not is_valid_bittensor_address_or_public_key(destination):
logging.error(
@@ -132,7 +119,9 @@ async def do_transfer() -> tuple[bool, str, str]:
subtensor.get_existential_deposit(block_hash=block_hash),
)
account_balance = account_balance_[wallet.coldkeypub.ss58_address]
- fee = await get_transfer_fee()
+ fee = await subtensor.get_transfer_fee(
+ wallet=wallet, dest=destination, value=amount.rao
+ )
if not keep_alive:
# Check if the transfer should keep_alive the account
@@ -153,7 +142,14 @@ async def do_transfer() -> tuple[bool, str, str]:
return False
logging.info(":satellite: Transferring...")
@@ -171,17 +167,13 @@ async def do_transfer() -> tuple[bool, str, str]:
logging.info(
f"[green]Taostats Explorer Link: {explorer_urls.get('taostats')}"
)
- else:
- logging.error(f":cross_mark: Failed: {err_msg}")
- if success:
logging.info(":satellite: Checking Balance...")
- new_balance = await subtensor.get_balance(
- wallet.coldkeypub.ss58_address, reuse_block=False
- )
+ new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address)
logging.info(
f"Balance: [blue]{account_balance} :arrow_right: [green]{new_balance[wallet.coldkeypub.ss58_address]}"
)
return True
-
- return False
+ else:
+ logging.error(f":cross_mark: Failed: {err_msg}")
+ return False
diff --git a/tests/unit_tests/extrinsics/test_async_transfer.py b/tests/unit_tests/extrinsics/test_async_transfer.py
new file mode 100644
index 000000000..91e6bfc49
--- /dev/null
+++ b/tests/unit_tests/extrinsics/test_async_transfer.py
@@ -0,0 +1,503 @@
+import pytest
+from bittensor.core import async_subtensor
+from bittensor_wallet import Wallet
+from bittensor.core.extrinsics import async_transfer
+from bittensor.utils.balance import Balance
+
+
+@pytest.fixture(autouse=True)
+def subtensor(mocker):
+ fake_async_substrate = mocker.AsyncMock(
+ autospec=async_subtensor.AsyncSubstrateInterface
+ )
+ mocker.patch.object(
+ async_subtensor, "AsyncSubstrateInterface", return_value=fake_async_substrate
+ )
+ return async_subtensor.AsyncSubtensor()
+
+
+@pytest.mark.asyncio
+async def test_do_transfer_success(subtensor, mocker):
+ """Tests _do_transfer when the transfer is successful."""
+ # Preps
+ fake_wallet = mocker.Mock(autospec=Wallet)
+ fake_destination = "destination_address"
+ fake_amount = mocker.Mock(autospec=Balance, rao=1000)
+
+ fake_call = mocker.AsyncMock()
+ fake_extrinsic = mocker.AsyncMock()
+ fake_response = mocker.Mock()
+
+ fake_response.is_success = mocker.AsyncMock(return_value=True)()
+ fake_response.process_events = mocker.AsyncMock()
+ fake_response.block_hash = "fake_block_hash"
+
+ mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call)
+ mocker.patch.object(
+ subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic
+ )
+ mocker.patch.object(
+ subtensor.substrate, "submit_extrinsic", return_value=fake_response
+ )
+
+ # Call
+ success, block_hash, error_message = await async_transfer._do_transfer(
+ subtensor=subtensor,
+ wallet=fake_wallet,
+ destination=fake_destination,
+ amount=fake_amount,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ )
+
+ # Asserts
+ subtensor.substrate.compose_call.assert_called_once_with(
+ call_module="Balances",
+ call_function="transfer_allow_death",
+ call_params={"dest": fake_destination, "value": fake_amount.rao},
+ )
+ subtensor.substrate.create_signed_extrinsic.assert_called_once_with(
+ call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey
+ )
+ subtensor.substrate.submit_extrinsic.assert_called_once_with(
+ subtensor.substrate.create_signed_extrinsic.return_value,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ )
+ assert success is True
+ assert block_hash == "fake_block_hash"
+ assert error_message == "Success with response."
+
+
+@pytest.mark.asyncio
+async def test_do_transfer_failure(subtensor, mocker):
+ """Tests _do_transfer when the transfer fails."""
+ # Preps
+ fake_wallet = mocker.Mock(autospec=Wallet)
+ fake_destination = "destination_address"
+ fake_amount = mocker.Mock(autospec=Balance, rao=1000)
+
+ fake_call = mocker.AsyncMock()
+ fake_extrinsic = mocker.AsyncMock()
+ fake_response = mocker.Mock()
+
+ fake_response.is_success = mocker.AsyncMock(return_value=False)()
+ fake_response.process_events = mocker.AsyncMock()
+ fake_response.error_message = mocker.AsyncMock(return_value="Fake error message")()
+
+ mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call)
+ mocker.patch.object(
+ subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic
+ )
+ mocker.patch.object(
+ subtensor.substrate, "submit_extrinsic", return_value=fake_response
+ )
+
+ mocked_format_error_message = mocker.patch.object(
+ async_transfer,
+ "format_error_message",
+ return_value="Formatted error message",
+ )
+
+ # Call
+ success, block_hash, error_message = await async_transfer._do_transfer(
+ subtensor=subtensor,
+ wallet=fake_wallet,
+ destination=fake_destination,
+ amount=fake_amount,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ )
+
+ # Asserts
+ subtensor.substrate.compose_call.assert_called_once_with(
+ call_module="Balances",
+ call_function="transfer_allow_death",
+ call_params={"dest": fake_destination, "value": fake_amount.rao},
+ )
+ subtensor.substrate.create_signed_extrinsic.assert_called_once_with(
+ call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey
+ )
+ subtensor.substrate.submit_extrinsic.assert_called_once_with(
+ subtensor.substrate.create_signed_extrinsic.return_value,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ )
+ assert success is False
+ assert block_hash == ""
+ mocked_format_error_message.assert_called_once_with(
+ "Fake error message", substrate=subtensor.substrate
+ )
+ assert error_message == "Formatted error message"
+
+
+@pytest.mark.asyncio
+async def test_do_transfer_no_waiting(subtensor, mocker):
+ """Tests _do_transfer when no waiting for inclusion or finalization."""
+ # Preps
+ fake_wallet = mocker.Mock(autospec=Wallet)
+ fake_destination = "destination_address"
+ fake_amount = mocker.Mock(autospec=Balance, rao=1000)
+
+ fake_call = mocker.AsyncMock()
+ fake_extrinsic = mocker.AsyncMock()
+
+ mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call)
+ mocker.patch.object(
+ subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic
+ )
+ mocker.patch.object(
+ subtensor.substrate,
+ "submit_extrinsic",
+ return_value=mocker.Mock(),
+ )
+
+ # Call
+ success, block_hash, error_message = await async_transfer._do_transfer(
+ subtensor=subtensor,
+ wallet=fake_wallet,
+ destination=fake_destination,
+ amount=fake_amount,
+ wait_for_inclusion=False,
+ wait_for_finalization=False,
+ )
+
+ # Asserts
+ subtensor.substrate.compose_call.assert_called_once_with(
+ call_module="Balances",
+ call_function="transfer_allow_death",
+ call_params={"dest": fake_destination, "value": fake_amount.rao},
+ )
+ subtensor.substrate.create_signed_extrinsic.assert_called_once_with(
+ call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey
+ )
+ subtensor.substrate.submit_extrinsic.assert_called_once_with(
+ subtensor.substrate.create_signed_extrinsic.return_value,
+ wait_for_inclusion=False,
+ wait_for_finalization=False,
+ )
+ assert success is True
+ assert block_hash == ""
+ assert error_message == "Success, extrinsic submitted without waiting."
+
+
+@pytest.mark.asyncio
+async def test_transfer_extrinsic_success(subtensor, mocker):
+ """Tests successful transfer."""
+ # Preps
+ fake_wallet = mocker.Mock(autospec=Wallet)
+ fake_wallet.coldkeypub.ss58_address = "fake_ss58_address"
+ fake_destination = "valid_ss58_address"
+ fake_amount = Balance(15)
+
+ mocked_is_valid_address = mocker.patch.object(
+ async_transfer,
+ "is_valid_bittensor_address_or_public_key",
+ return_value=True,
+ )
+ mocked_unlock_key = mocker.patch.object(
+ async_transfer,
+ "unlock_key",
+ return_value=mocker.Mock(success=True, message="Unlocked"),
+ )
+ mocked_get_chain_head = mocker.patch.object(
+ subtensor.substrate, "get_chain_head", return_value="some_block_hash"
+ )
+ mocked_get_balance = mocker.patch.object(
+ subtensor,
+ "get_balance",
+ return_value={fake_wallet.coldkeypub.ss58_address: 10000},
+ )
+ mocked_get_existential_deposit = mocker.patch.object(
+ subtensor, "get_existential_deposit", return_value=1
+ )
+ subtensor.get_transfer_fee = mocker.patch.object(
+ subtensor, "get_transfer_fee", return_value=2
+ )
+ mocked_do_transfer = mocker.patch.object(
+ async_transfer, "_do_transfer", return_value=(True, "fake_block_hash", "")
+ )
+
+ # Call
+ result = await async_transfer.transfer_extrinsic(
+ subtensor=subtensor,
+ wallet=fake_wallet,
+ destination=fake_destination,
+ amount=fake_amount,
+ transfer_all=False,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ keep_alive=True,
+ )
+
+ # Asserts
+ mocked_is_valid_address.assert_called_once_with(fake_destination)
+ mocked_unlock_key.assert_called_once_with(fake_wallet)
+ mocked_get_chain_head.assert_called_once()
+ mocked_get_balance.assert_called_with(
+ fake_wallet.coldkeypub.ss58_address,
+ )
+ mocked_get_existential_deposit.assert_called_once_with(
+ block_hash=mocked_get_chain_head.return_value
+ )
+ mocked_do_transfer.assert_called_once()
+ assert result is True
+
+
+@pytest.mark.asyncio
+async def test_transfer_extrinsic_call_successful_with_failed_response(
+ subtensor, mocker
+):
+ """Tests successful transfer call is successful with failed response."""
+ # Preps
+ fake_wallet = mocker.Mock(autospec=Wallet)
+ fake_wallet.coldkeypub.ss58_address = "fake_ss58_address"
+ fake_destination = "valid_ss58_address"
+ fake_amount = Balance(15)
+
+ mocked_is_valid_address = mocker.patch.object(
+ async_transfer,
+ "is_valid_bittensor_address_or_public_key",
+ return_value=True,
+ )
+ mocked_unlock_key = mocker.patch.object(
+ async_transfer,
+ "unlock_key",
+ return_value=mocker.Mock(success=True, message="Unlocked"),
+ )
+ mocked_get_chain_head = mocker.patch.object(
+ subtensor.substrate, "get_chain_head", return_value="some_block_hash"
+ )
+ mocked_get_balance = mocker.patch.object(
+ subtensor,
+ "get_balance",
+ return_value={fake_wallet.coldkeypub.ss58_address: 10000},
+ )
+ mocked_get_existential_deposit = mocker.patch.object(
+ subtensor, "get_existential_deposit", return_value=1
+ )
+ subtensor.get_transfer_fee = mocker.patch.object(
+ subtensor, "get_transfer_fee", return_value=2
+ )
+ mocked_do_transfer = mocker.patch.object(
+ async_transfer, "_do_transfer", return_value=(False, "", "")
+ )
+
+ # Call
+ result = await async_transfer.transfer_extrinsic(
+ subtensor=subtensor,
+ wallet=fake_wallet,
+ destination=fake_destination,
+ amount=fake_amount,
+ transfer_all=False,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ keep_alive=True,
+ )
+
+ # Asserts
+ mocked_is_valid_address.assert_called_once_with(fake_destination)
+ mocked_unlock_key.assert_called_once_with(fake_wallet)
+ mocked_get_chain_head.assert_called_once()
+ mocked_get_balance.assert_called_with(
+ fake_wallet.coldkeypub.ss58_address,
+ block_hash=mocked_get_chain_head.return_value,
+ )
+ mocked_get_existential_deposit.assert_called_once_with(
+ block_hash=mocked_get_chain_head.return_value
+ )
+ mocked_do_transfer.assert_called_once()
+ assert result is False
+
+
+@pytest.mark.asyncio
+async def test_transfer_extrinsic_insufficient_balance(subtensor, mocker):
+ """Tests transfer when balance is insufficient."""
+ # Preps
+ fake_wallet = mocker.Mock(autospec=Wallet)
+ fake_wallet.coldkeypub.ss58_address = "fake_ss58_address"
+ fake_destination = "valid_ss58_address"
+ fake_amount = Balance(5000)
+
+ mocked_is_valid_address = mocker.patch.object(
+ async_transfer,
+ "is_valid_bittensor_address_or_public_key",
+ return_value=True,
+ )
+ mocked_unlock_key = mocker.patch.object(
+ async_transfer,
+ "unlock_key",
+ return_value=mocker.Mock(success=True, message="Unlocked"),
+ )
+ mocked_get_chain_head = mocker.patch.object(
+ subtensor.substrate, "get_chain_head", return_value="some_block_hash"
+ )
+ mocked_get_balance = mocker.patch.object(
+ subtensor,
+ "get_balance",
+ return_value={
+ fake_wallet.coldkeypub.ss58_address: 1000
+ }, # Insufficient balance
+ )
+ mocked_get_existential_deposit = mocker.patch.object(
+ subtensor, "get_existential_deposit", return_value=1
+ )
+ subtensor.get_transfer_fee = mocker.patch.object(
+ subtensor, "get_transfer_fee", return_value=2
+ )
+
+ # Call
+ result = await async_transfer.transfer_extrinsic(
+ subtensor=subtensor,
+ wallet=fake_wallet,
+ destination=fake_destination,
+ amount=fake_amount,
+ transfer_all=False,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ keep_alive=True,
+ )
+
+ # Asserts
+ mocked_is_valid_address.assert_called_once_with(fake_destination)
+ mocked_unlock_key.assert_called_once_with(fake_wallet)
+ mocked_get_chain_head.assert_called_once()
+ mocked_get_balance.assert_called_once()
+ mocked_get_existential_deposit.assert_called_once_with(
+ block_hash=mocked_get_chain_head.return_value
+ )
+ assert result is False
+
+
+@pytest.mark.asyncio
+async def test_transfer_extrinsic_invalid_destination(subtensor, mocker):
+ """Tests transfer with invalid destination address."""
+ # Preps
+ fake_wallet = mocker.Mock(autospec=Wallet)
+ fake_wallet.coldkeypub.ss58_address = "fake_ss58_address"
+ fake_destination = "invalid_address"
+ fake_amount = Balance(15)
+
+ mocked_is_valid_address = mocker.patch.object(
+ async_transfer,
+ "is_valid_bittensor_address_or_public_key",
+ return_value=False,
+ )
+
+ # Call
+ result = await async_transfer.transfer_extrinsic(
+ subtensor=subtensor,
+ wallet=fake_wallet,
+ destination=fake_destination,
+ amount=fake_amount,
+ transfer_all=False,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ keep_alive=True,
+ )
+
+ # Asserts
+ mocked_is_valid_address.assert_called_once_with(fake_destination)
+ assert result is False
+
+
+@pytest.mark.asyncio
+async def test_transfer_extrinsic_unlock_key_false(subtensor, mocker):
+ """Tests transfer failed unlock_key."""
+ # Preps
+ fake_wallet = mocker.Mock(autospec=Wallet)
+ fake_wallet.coldkeypub.ss58_address = "fake_ss58_address"
+ fake_destination = "invalid_address"
+ fake_amount = Balance(15)
+
+ mocked_is_valid_address = mocker.patch.object(
+ async_transfer,
+ "is_valid_bittensor_address_or_public_key",
+ return_value=True,
+ )
+
+ mocked_unlock_key = mocker.patch.object(
+ async_transfer,
+ "unlock_key",
+ return_value=mocker.Mock(success=False, message=""),
+ )
+
+ # Call
+ result = await async_transfer.transfer_extrinsic(
+ subtensor=subtensor,
+ wallet=fake_wallet,
+ destination=fake_destination,
+ amount=fake_amount,
+ transfer_all=False,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ keep_alive=True,
+ )
+
+ # Asserts
+ mocked_is_valid_address.assert_called_once_with(fake_destination)
+ mocked_unlock_key.assert_called_once_with(fake_wallet)
+ assert result is False
+
+
+@pytest.mark.asyncio
+async def test_transfer_extrinsic_keep_alive_false_and_transfer_all_true(
+ subtensor, mocker
+):
+ """Tests transfer with keep_alive flag set to False and transfer_all flag set to True."""
+ # Preps
+ fake_wallet = mocker.Mock(autospec=Wallet)
+ fake_wallet.coldkeypub.ss58_address = "fake_ss58_address"
+ fake_destination = "valid_ss58_address"
+ fake_amount = Balance(15)
+
+ mocked_is_valid_address = mocker.patch.object(
+ async_transfer,
+ "is_valid_bittensor_address_or_public_key",
+ return_value=True,
+ )
+ mocked_unlock_key = mocker.patch.object(
+ async_transfer,
+ "unlock_key",
+ return_value=mocker.Mock(success=True, message="Unlocked"),
+ )
+ mocked_get_chain_head = mocker.patch.object(
+ subtensor.substrate, "get_chain_head", return_value="some_block_hash"
+ )
+ mocked_get_balance = mocker.patch.object(
+ subtensor,
+ "get_balance",
+ return_value={fake_wallet.coldkeypub.ss58_address: 1},
+ )
+ mocked_get_existential_deposit = mocker.patch.object(
+ subtensor, "get_existential_deposit", return_value=1
+ )
+ subtensor.get_transfer_fee = mocker.patch.object(
+ subtensor, "get_transfer_fee", return_value=2
+ )
+ mocked_do_transfer = mocker.patch.object(
+ async_transfer, "_do_transfer", return_value=(True, "fake_block_hash", "")
+ )
+
+ # Call
+ result = await async_transfer.transfer_extrinsic(
+ subtensor=subtensor,
+ wallet=fake_wallet,
+ destination=fake_destination,
+ amount=fake_amount,
+ transfer_all=True,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+ keep_alive=False,
+ )
+
+ # Asserts
+ mocked_is_valid_address.assert_called_once_with(fake_destination)
+ mocked_unlock_key.assert_called_once_with(fake_wallet)
+ mocked_get_chain_head.assert_called_once()
+
+ mocked_get_existential_deposit.assert_called_once_with(
+ block_hash=mocked_get_chain_head.return_value
+ )
+ mocked_do_transfer.assert_not_called()
+ assert result is False