2022-09-30 11:40:22 +03:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-11-23 00:34:53 +03:00
|
|
|
import logging
|
2022-10-24 22:03:53 +03:00
|
|
|
import traceback
|
2022-08-03 23:14:58 +03:00
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
from pathlib import Path
|
2023-02-28 08:49:47 +03:00
|
|
|
from typing import Any, AsyncIterator, Awaitable, Callable, Dict, List, Optional, Tuple, Type, TypeVar
|
2022-08-01 18:54:59 +03:00
|
|
|
|
2022-08-03 23:14:58 +03:00
|
|
|
from aiohttp import ClientConnectorError
|
2022-08-01 18:54:59 +03:00
|
|
|
|
2022-11-23 00:34:53 +03:00
|
|
|
from chia.daemon.keychain_proxy import KeychainProxy, connect_to_keychain_and_validate
|
2022-10-20 01:02:05 +03:00
|
|
|
from chia.rpc.data_layer_rpc_client import DataLayerRpcClient
|
2022-08-03 23:14:58 +03:00
|
|
|
from chia.rpc.farmer_rpc_client import FarmerRpcClient
|
|
|
|
from chia.rpc.full_node_rpc_client import FullNodeRpcClient
|
|
|
|
from chia.rpc.harvester_rpc_client import HarvesterRpcClient
|
|
|
|
from chia.rpc.rpc_client import RpcClient
|
2022-08-01 18:54:59 +03:00
|
|
|
from chia.rpc.wallet_rpc_client import WalletRpcClient
|
2022-04-20 21:39:38 +03:00
|
|
|
from chia.types.blockchain_format.sized_bytes import bytes32
|
|
|
|
from chia.types.mempool_submission_status import MempoolSubmissionStatus
|
2022-08-01 18:54:59 +03:00
|
|
|
from chia.util.config import load_config
|
|
|
|
from chia.util.default_root import DEFAULT_ROOT_PATH
|
|
|
|
from chia.util.ints import uint16
|
2022-11-23 00:34:53 +03:00
|
|
|
from chia.util.keychain import KeyData
|
2022-04-20 21:39:38 +03:00
|
|
|
from chia.wallet.transaction_record import TransactionRecord
|
|
|
|
|
2022-08-03 23:14:58 +03:00
|
|
|
NODE_TYPES: Dict[str, Type[RpcClient]] = {
|
|
|
|
"farmer": FarmerRpcClient,
|
|
|
|
"wallet": WalletRpcClient,
|
|
|
|
"full_node": FullNodeRpcClient,
|
|
|
|
"harvester": HarvesterRpcClient,
|
2022-10-20 01:02:05 +03:00
|
|
|
"data_layer": DataLayerRpcClient,
|
2022-08-03 23:14:58 +03:00
|
|
|
}
|
|
|
|
|
2023-02-28 08:49:47 +03:00
|
|
|
node_config_section_names: Dict[Type[RpcClient], str] = {
|
|
|
|
FarmerRpcClient: "farmer",
|
|
|
|
WalletRpcClient: "wallet",
|
|
|
|
FullNodeRpcClient: "full_node",
|
|
|
|
HarvesterRpcClient: "harvester",
|
|
|
|
DataLayerRpcClient: "data_layer",
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_T_RpcClient = TypeVar("_T_RpcClient", bound=RpcClient)
|
|
|
|
|
2022-04-20 21:39:38 +03:00
|
|
|
|
|
|
|
def transaction_submitted_msg(tx: TransactionRecord) -> str:
|
|
|
|
sent_to = [MempoolSubmissionStatus(s[0], s[1], s[2]).to_json_dict_convenience() for s in tx.sent_to]
|
|
|
|
return f"Transaction submitted to nodes: {sent_to}"
|
|
|
|
|
|
|
|
|
|
|
|
def transaction_status_msg(fingerprint: int, tx_id: bytes32) -> str:
|
|
|
|
return f"Run 'chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}' to get status"
|
2022-08-01 18:54:59 +03:00
|
|
|
|
|
|
|
|
2022-08-03 23:14:58 +03:00
|
|
|
async def validate_client_connection(
|
2022-11-23 00:34:53 +03:00
|
|
|
rpc_client: RpcClient,
|
|
|
|
node_type: str,
|
|
|
|
rpc_port: int,
|
|
|
|
root_path: Path,
|
|
|
|
fingerprint: Optional[int],
|
|
|
|
login_to_wallet: bool,
|
2022-08-03 23:14:58 +03:00
|
|
|
) -> Optional[int]:
|
|
|
|
try:
|
|
|
|
await rpc_client.healthz()
|
2022-09-03 01:08:48 +03:00
|
|
|
if type(rpc_client) == WalletRpcClient and login_to_wallet:
|
2022-11-23 00:34:53 +03:00
|
|
|
fingerprint = await get_wallet(root_path, rpc_client, fingerprint)
|
2022-08-03 23:14:58 +03:00
|
|
|
if fingerprint is None:
|
|
|
|
rpc_client.close()
|
|
|
|
except ClientConnectorError:
|
|
|
|
print(f"Connection error. Check if {node_type.replace('_', ' ')} rpc is running at {rpc_port}")
|
|
|
|
print(f"This is normal if {node_type.replace('_', ' ')} is still starting up")
|
|
|
|
rpc_client.close()
|
|
|
|
await rpc_client.await_closed() # if close is not already called this does nothing
|
|
|
|
return fingerprint
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
async def get_any_service_client(
|
2023-02-28 08:49:47 +03:00
|
|
|
client_type: Type[_T_RpcClient],
|
2022-08-03 23:14:58 +03:00
|
|
|
rpc_port: Optional[int] = None,
|
|
|
|
root_path: Path = DEFAULT_ROOT_PATH,
|
|
|
|
fingerprint: Optional[int] = None,
|
2022-09-03 01:08:48 +03:00
|
|
|
login_to_wallet: bool = True,
|
2023-02-28 08:49:47 +03:00
|
|
|
) -> AsyncIterator[Tuple[Optional[_T_RpcClient], Dict[str, Any], Optional[int]]]:
|
2022-08-03 23:14:58 +03:00
|
|
|
"""
|
|
|
|
Yields a tuple with a RpcClient for the applicable node type a dictionary of the node's configuration,
|
|
|
|
and a fingerprint if applicable. However, if connecting to the node fails then we will return None for
|
|
|
|
the RpcClient.
|
|
|
|
"""
|
|
|
|
|
2023-02-28 08:49:47 +03:00
|
|
|
node_type = node_config_section_names.get(client_type)
|
|
|
|
if node_type is None:
|
2022-08-03 23:14:58 +03:00
|
|
|
# Click already checks this, so this should never happen
|
2023-02-28 08:49:47 +03:00
|
|
|
raise ValueError(f"Invalid client type requested: {client_type.__name__}")
|
2022-08-03 23:14:58 +03:00
|
|
|
# load variables from config file
|
2023-02-28 08:49:47 +03:00
|
|
|
config = load_config(root_path, "config.yaml", fill_missing_services=issubclass(client_type, DataLayerRpcClient))
|
2022-08-03 23:14:58 +03:00
|
|
|
self_hostname = config["self_hostname"]
|
|
|
|
if rpc_port is None:
|
|
|
|
rpc_port = config[node_type]["rpc_port"]
|
|
|
|
# select node client type based on string
|
2023-02-28 08:49:47 +03:00
|
|
|
node_client = await client_type.create(self_hostname, uint16(rpc_port), root_path, config)
|
2022-08-01 18:54:59 +03:00
|
|
|
try:
|
2022-08-03 23:14:58 +03:00
|
|
|
# check if we can connect to node, and if we can then validate
|
|
|
|
# fingerprint access, otherwise return fingerprint and shutdown client
|
2022-11-23 00:34:53 +03:00
|
|
|
fingerprint = await validate_client_connection(
|
|
|
|
node_client, node_type, rpc_port, root_path, fingerprint, login_to_wallet
|
|
|
|
)
|
2022-08-03 23:14:58 +03:00
|
|
|
if node_client.session.closed:
|
|
|
|
yield None, config, fingerprint
|
2022-08-01 18:54:59 +03:00
|
|
|
else:
|
2022-08-03 23:14:58 +03:00
|
|
|
yield node_client, config, fingerprint
|
|
|
|
except Exception as e: # this is only here to make the errors more user-friendly.
|
2022-10-24 22:03:53 +03:00
|
|
|
print(f"Exception from '{node_type}' {e}:\n{traceback.format_exc()}")
|
|
|
|
|
2022-08-03 23:14:58 +03:00
|
|
|
finally:
|
|
|
|
node_client.close() # this can run even if already closed, will just do nothing.
|
|
|
|
await node_client.await_closed()
|
2022-08-01 18:54:59 +03:00
|
|
|
|
|
|
|
|
2022-11-23 00:34:53 +03:00
|
|
|
async def get_wallet(root_path: Path, wallet_client: WalletRpcClient, fingerprint: Optional[int]) -> Optional[int]:
|
|
|
|
selected_fingerprint: Optional[int] = None
|
|
|
|
keychain_proxy: Optional[KeychainProxy] = None
|
|
|
|
all_keys: List[KeyData] = []
|
|
|
|
|
2023-02-22 11:51:26 +03:00
|
|
|
try:
|
|
|
|
if fingerprint is not None:
|
|
|
|
selected_fingerprint = fingerprint
|
|
|
|
else:
|
|
|
|
keychain_proxy = await connect_to_keychain_and_validate(root_path, log=logging.getLogger(__name__))
|
|
|
|
if keychain_proxy is None:
|
|
|
|
raise RuntimeError("Failed to connect to keychain")
|
|
|
|
# we're only interested in the fingerprints and labels
|
|
|
|
all_keys = await keychain_proxy.get_keys(include_secrets=False)
|
|
|
|
# we don't immediately close the keychain proxy connection because it takes a noticeable amount of time
|
|
|
|
fingerprints = [key.fingerprint for key in all_keys]
|
|
|
|
if len(fingerprints) == 0:
|
|
|
|
print("No keys loaded. Run 'chia keys generate' or import a key")
|
|
|
|
elif len(fingerprints) == 1:
|
|
|
|
# if only a single key is available, select it automatically
|
|
|
|
selected_fingerprint = fingerprints[0]
|
|
|
|
|
|
|
|
if selected_fingerprint is None and len(all_keys) > 0:
|
|
|
|
logged_in_fingerprint: Optional[int] = await wallet_client.get_logged_in_fingerprint()
|
|
|
|
logged_in_key: Optional[KeyData] = None
|
|
|
|
if logged_in_fingerprint is not None:
|
|
|
|
logged_in_key = next((key for key in all_keys if key.fingerprint == logged_in_fingerprint), None)
|
|
|
|
current_sync_status: str = ""
|
|
|
|
indent = " "
|
|
|
|
if logged_in_key is not None:
|
|
|
|
if await wallet_client.get_synced():
|
|
|
|
current_sync_status = "Synced"
|
|
|
|
elif await wallet_client.get_sync_status():
|
|
|
|
current_sync_status = "Syncing"
|
|
|
|
else:
|
|
|
|
current_sync_status = "Not Synced"
|
|
|
|
|
|
|
|
print()
|
|
|
|
print("Active Wallet Key (*):")
|
|
|
|
print(f"{indent}{'-Fingerprint:'.ljust(23)} {logged_in_key.fingerprint}")
|
|
|
|
if logged_in_key.label is not None:
|
|
|
|
print(f"{indent}{'-Label:'.ljust(23)} {logged_in_key.label}")
|
|
|
|
print(f"{indent}{'-Sync Status:'.ljust(23)} {current_sync_status}")
|
|
|
|
max_key_index_width = 5 # e.g. "12) *", "1) *", or "2) "
|
|
|
|
max_fingerprint_width = 10 # fingerprint is a 32-bit number
|
2022-11-23 00:34:53 +03:00
|
|
|
print()
|
2023-02-22 11:51:26 +03:00
|
|
|
print("Wallet Keys:")
|
|
|
|
for i, key in enumerate(all_keys):
|
|
|
|
key_index_str = f"{(str(i + 1) + ')'):<4}"
|
|
|
|
key_index_str += "*" if key.fingerprint == logged_in_fingerprint else " "
|
|
|
|
print(
|
|
|
|
f"{key_index_str:<{max_key_index_width}} "
|
|
|
|
f"{key.fingerprint:<{max_fingerprint_width}}"
|
|
|
|
f"{(indent + key.label) if key.label else ''}"
|
|
|
|
)
|
|
|
|
val = None
|
|
|
|
prompt: str = (
|
|
|
|
f"Choose a wallet key [1-{len(fingerprints)}] ('q' to quit, or Enter to use {logged_in_fingerprint}): "
|
2022-11-23 00:34:53 +03:00
|
|
|
)
|
2023-02-22 11:51:26 +03:00
|
|
|
while val is None:
|
|
|
|
val = input(prompt)
|
|
|
|
if val == "q":
|
|
|
|
break
|
|
|
|
elif val == "" and logged_in_fingerprint is not None:
|
|
|
|
fingerprint = logged_in_fingerprint
|
|
|
|
break
|
|
|
|
elif not val.isdigit():
|
2022-08-01 18:54:59 +03:00
|
|
|
val = None
|
|
|
|
else:
|
2023-02-22 11:51:26 +03:00
|
|
|
index = int(val) - 1
|
|
|
|
if index < 0 or index >= len(fingerprints):
|
|
|
|
print("Invalid value")
|
|
|
|
val = None
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
fingerprint = fingerprints[index]
|
|
|
|
|
|
|
|
selected_fingerprint = fingerprint
|
|
|
|
|
|
|
|
if selected_fingerprint is not None:
|
|
|
|
log_in_response = await wallet_client.log_in(selected_fingerprint)
|
|
|
|
|
|
|
|
if log_in_response["success"] is False:
|
|
|
|
print(f"Login failed for fingerprint {selected_fingerprint}: {log_in_response}")
|
|
|
|
selected_fingerprint = None
|
|
|
|
finally:
|
|
|
|
# Closing the keychain proxy takes a moment, so we wait until after the login is complete
|
|
|
|
if keychain_proxy is not None:
|
|
|
|
await keychain_proxy.close()
|
2022-11-23 00:34:53 +03:00
|
|
|
|
|
|
|
return selected_fingerprint
|
2022-08-03 23:14:58 +03:00
|
|
|
|
|
|
|
|
|
|
|
async def execute_with_wallet(
|
|
|
|
wallet_rpc_port: Optional[int],
|
|
|
|
fingerprint: int,
|
|
|
|
extra_params: Dict[str, Any],
|
|
|
|
function: Callable[[Dict[str, Any], WalletRpcClient, int], Awaitable[None]],
|
|
|
|
) -> None:
|
2023-02-28 08:49:47 +03:00
|
|
|
async with get_any_service_client(WalletRpcClient, wallet_rpc_port, fingerprint=fingerprint) as (
|
|
|
|
wallet_client,
|
|
|
|
_,
|
|
|
|
new_fp,
|
|
|
|
):
|
2022-08-03 23:14:58 +03:00
|
|
|
if wallet_client is not None:
|
|
|
|
assert new_fp is not None # wallet only sanity check
|
|
|
|
await function(extra_params, wallet_client, new_fp)
|