# The MIT License (MIT)
# Copyright © 2024 Opentensor Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
# documentation files (the “Software”), to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of
# the Software.
#
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
from typing import Optional, Union, TYPE_CHECKING
from retry import retry
from bittensor.core.extrinsics.utils import submit_extrinsic
from bittensor.core.settings import NETWORK_EXPLORER_MAP
from bittensor.utils import (
get_explorer_url_for_network,
format_error_message,
is_valid_bittensor_address_or_public_key,
)
from bittensor.utils.balance import Balance
from bittensor.utils.btlogging import logging
from bittensor.utils.networking import ensure_connected
# For annotation purposes
if TYPE_CHECKING:
from bittensor.core.subtensor import Subtensor
from bittensor_wallet import Wallet
# Chain call for `transfer_extrinsic`
[docs]
@ensure_connected
def do_transfer(
self: "Subtensor",
wallet: "Wallet",
dest: str,
transfer_balance: "Balance",
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
) -> tuple[bool, Optional[str], Optional[dict]]:
"""Sends a transfer extrinsic to the chain.
Args:
self (subtensor.core.subtensor.Subtensor): The Subtensor instance object.
wallet (bittensor_wallet.Wallet): Wallet object.
dest (str): Destination public key address.
transfer_balance (bittensor.utils.balance.Balance): Amount to transfer.
wait_for_inclusion (bool): If ``true``, waits for inclusion.
wait_for_finalization (bool): If ``true``, waits for finalization.
Returns:
success (bool): ``True`` if transfer was successful.
block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is ``True``.
error (dict): Error message from subtensor if transfer failed.
"""
@retry(delay=1, tries=3, backoff=2, max_delay=4)
def make_substrate_call_with_retry():
call = self.substrate.compose_call(
call_module="Balances",
call_function="transfer_allow_death",
call_params={"dest": dest, "value": transfer_balance.rao},
)
extrinsic = self.substrate.create_signed_extrinsic(
call=call, keypair=wallet.coldkey
)
response = submit_extrinsic(
substrate=self.substrate,
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, None, None
# Otherwise continue with finalization.
response.process_events()
if response.is_success:
block_hash = response.block_hash
return True, block_hash, None
else:
return False, None, response.error_message
return make_substrate_call_with_retry()
# Community uses this extrinsic directly and via `subtensor.transfer`
[docs]
def transfer_extrinsic(
subtensor: "Subtensor",
wallet: "Wallet",
dest: str,
amount: Union["Balance", float],
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
keep_alive: bool = True,
) -> bool:
"""Transfers funds from this wallet to the destination public key address.
Args:
subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object.
wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from.
dest (str, ss58_address or ed25519): Destination public key address of receiver.
amount (Union[Balance, int]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao.
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.
keep_alive (bool): If set, keeps the account alive by keeping the balance above the existential deposit.
Returns:
success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``.
"""
# Validate destination address.
if not is_valid_bittensor_address_or_public_key(dest):
logging.error(f"<red>Invalid destination address: {dest}</red>")
return False
if isinstance(dest, bytes):
# Convert bytes to hex string.
dest = "0x" + dest.hex()
# Unlock wallet coldkey.
wallet.unlock_coldkey()
# Convert to bittensor.Balance
if not isinstance(amount, Balance):
transfer_balance = Balance.from_tao(amount)
else:
transfer_balance = amount
# Check balance.
logging.info(":satellite: <magenta>Checking Balance...</magenta>")
account_balance = subtensor.get_balance(wallet.coldkey.ss58_address)
# check existential deposit.
existential_deposit = subtensor.get_existential_deposit()
logging.info(":satellite: <magenta>Transferring...</magenta>")
fee = subtensor.get_transfer_fee(
wallet=wallet, dest=dest, value=transfer_balance.rao
)
if not keep_alive:
# Check if the transfer should keep_alive the account
existential_deposit = Balance(0)
# Check if we have enough balance.
if account_balance < (transfer_balance + fee + existential_deposit):
logging.error(":cross_mark: <red>Not enough balance</red>:")
logging.info(f"\t\tBalance: \t<blue>{account_balance}</blue>")
logging.info(f"\t\tAmount: \t<blue>{transfer_balance}</blue>")
logging.info(f"\t\tFor fee: \t<blue>{fee}</blue>")
return False
logging.info(":satellite: <magenta>Transferring...</magenta>")
logging.info(f"\tAmount: <blue>{transfer_balance}</blue>")
logging.info(f"\tfrom: <blue>{wallet.name}:{wallet.coldkey.ss58_address}</blue>")
logging.info(f"\tTo: <blue>{dest}</blue>")
logging.info(f"\tFor fee: <blue>{fee}</blue>")
success, block_hash, error_message = do_transfer(
self=subtensor,
wallet=wallet,
dest=dest,
transfer_balance=transfer_balance,
wait_for_finalization=wait_for_finalization,
wait_for_inclusion=wait_for_inclusion,
)
if success:
logging.success(":white_heavy_check_mark: <green>Finalized</green>")
logging.info(f"<green>Block Hash:</green> <blue>{block_hash}</blue>")
explorer_urls = get_explorer_url_for_network(
subtensor.network, block_hash, NETWORK_EXPLORER_MAP
)
if explorer_urls != {} and explorer_urls:
logging.info(
f"<green>Opentensor Explorer Link: {explorer_urls.get('opentensor')}</green>"
)
logging.info(
f"<green>Taostats Explorer Link: {explorer_urls.get('taostats')}</green>"
)
else:
logging.error(
f":cross_mark: <red>Failed</red>: {format_error_message(error_message)}"
)
if success:
logging.info(":satellite: <magenta>Checking Balance...</magenta>")
new_balance = subtensor.get_balance(wallet.coldkey.ss58_address)
logging.success(
f"Balance: <blue>{account_balance}</blue> :arrow_right: <green>{new_balance}</green>"
)
return True
return False