Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wallets.py): add wallet history command #1638

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bittensor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"swap_hotkey": SwapHotkeyCommand,
"set_identity": SetIdentityCommand,
"get_identity": GetIdentityCommand,
"history": GetWalletHistoryCommand,
},
},
"stake": {
Expand Down
1 change: 1 addition & 0 deletions bittensor/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
UpdateWalletCommand,
WalletCreateCommand,
WalletBalanceCommand,
GetWalletHistoryCommand,
)
from .transfer import TransferCommand
from .inspect import InspectCommand
Expand Down
161 changes: 161 additions & 0 deletions bittensor/commands/wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from rich.table import Table
from typing import Optional, List
from . import defaults
import requests


class RegenColdkeyCommand:
Expand Down Expand Up @@ -827,3 +828,163 @@ def check_config(config: "bittensor.config"):
_,
config.subtensor.chain_endpoint,
) = bittensor.subtensor.determine_chain_endpoint_and_network(str(network))


API_URL = "https://api.subquery.network/sq/TaoStats/bittensor-indexer"
ifrit98 marked this conversation as resolved.
Show resolved Hide resolved

GRAPHQL_QUERY = """
query ($first: Int!, $after: Cursor, $filter: TransferFilter, $order: [TransfersOrderBy!]!) {
transfers(first: $first, after: $after, filter: $filter, orderBy: $order) {
nodes {
id
from
to
amount
extrinsicId
blockNumber
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
}
totalCount
}
}
"""


class GetWalletHistoryCommand:
"""
Executes the 'history' command to fetch the latest transfers of the provided wallet on the Bittensor network.

This command provides a detailed view of the transfers carried out on the wallet.

Usage:
The command lists the latest transfers of the provided wallet, showing the From, To, Amount, Extrinsic Id and Block Number.

Optional arguments:
None. The command uses the wallet and subtensor configurations to fetch latest transfer data associated with a wallet.

Example usage:
>>> btcli wallet history

Note:
This command is essential for users to monitor their financial status on the Bittensor network.
It helps in fetching info on all the transfers so that user can easily tally and cross check the transactions.
"""

@staticmethod
def run(cli):
r"""Check the transfer history of the provided wallet."""
wallet = bittensor.wallet(config=cli.config)
wallet_address = wallet.coldkey.ss58_address
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is somewhat cumbersome to enter the password each time to simply get the coldkey address, which is not exactly private data.

Suggestion:

wallet_address = wallet.get_coldkeypub().ss58_address

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done.


# Fetch all transfers
transfers = get_wallet_transfers(wallet_address)

# Create output table
table = create_transfer_history_table(transfers)

bittensor.__console__.print(table)

@staticmethod
def add_args(parser: argparse.ArgumentParser):
history_parser = parser.add_parser(
"history",
help="""Fetch transfer history associated with the provided wallet""",
)
bittensor.wallet.add_args(history_parser)
bittensor.subtensor.add_args(history_parser)

@staticmethod
def check_config(config: "bittensor.config"):
if not config.is_set("wallet.name") and not config.no_prompt:
wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name)
config.wallet.name = str(wallet_name)


def get_wallet_transfers(wallet_address) -> List[dict]:
"""Get all transfers associated with the provided wallet address."""

variables = {
"first": 10,
"filter": {
"or": [
{"from": {"equalTo": wallet_address}},
{"to": {"equalTo": wallet_address}},
]
},
"order": "BLOCK_NUMBER_DESC",
}

transfers = []

# Make the request with the provided query and variables until all transfers are fetched
while True:
Copy link
Contributor

@ifrit98 ifrit98 Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally weary of unbounded while True statements in python. This technically should terminate on the desired condition, but may want to explicitly put the exit condition in this line and/or specify some other conditions for exiting. For example, if there is significant txn volume, we may want to limit the output to n txns.

For example, adding a max_txn_history that we exit upon reaching:

max_txn_history = 1000 # this should be set in the config.
page_info = {"page_info": {"hasNextPage": True}} # for initial loop entry, will be filled by `transfer_data`
while len(transfers) < max_txn_history and page_info.get("hasNextPage") == True:
    ...

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think it will be a better approach. Will be fixing this in the next commit.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You not need while. Change 10 to 1000 or more

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alex0500 removed while and set max_txn to 1000.

response = requests.post(
API_URL, json={"query": GRAPHQL_QUERY, "variables": variables}
)
data = response.json()

# Extract nodes and pageInfo from the response
transfer_data = data.get("data", {}).get("transfers", {})
nodes = transfer_data.get("nodes", [])
page_info = transfer_data.get("pageInfo", {})

# Add the nodes to the result list
transfers.extend(nodes)

# Check if there are more pages
if not page_info.get("hasNextPage"):
break

# Update variables for the next request with the endCursor from the current response
variables["after"] = page_info["endCursor"]

return transfers


def create_transfer_history_table(transfers):
"""Get output transfer table"""

table = Table(show_footer=False)
# Define the column names
column_names = ["Id", "From", "To", "Amount", "Extrinsic Id", "Block Number"]

# Create a table
table = Table(show_footer=False)
table.title = "[white]Wallet Transfers"

# Define the column styles
header_style = "overline white"
footer_style = "overline white"
column_style = "rgb(50,163,219)"
no_wrap = True

# Add columns to the table
for column_name in column_names:
table.add_column(
f"[white]{column_name}",
header_style=header_style,
footer_style=footer_style,
style=column_style,
no_wrap=no_wrap,
)

# Add rows to the table
for item in transfers:
table.add_row(
item["id"],
item["from"],
item["to"],
item["amount"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current amount displayed is in rao, and displaying in tao may be more user-friendly. It's a simple divide by 1e9

Suggestion:

int(item["amount"]) / 1e9

May want to do some error handling if the value cannot be converted to an int.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also done. Tested it for the below values,
2
2.9
"2"
"2.989767"

Any invalid integer will throw an error, in that case, I am using it as it as. Any other suggestions on the same?

str(item["extrinsicId"]),
item["blockNumber"],
)
table.add_row()
table.show_footer = True
table.box = None
table.pad_edge = False
table.width = None
return table