diff --git a/cryptnox_cli/command/btc.py b/cryptnox_cli/command/btc.py index bf85502..1769997 100644 --- a/cryptnox_cli/command/btc.py +++ b/cryptnox_cli/command/btc.py @@ -6,6 +6,8 @@ import cryptnox_sdk_py +from concurrent.futures import ThreadPoolExecutor + from .command import Command from .helper.config import create_config_method from .helper.helper_methods import sign @@ -63,12 +65,17 @@ def _send(self, card: cryptnox_sdk_py.Card) -> None: if self.data.fees: fees = self.data.fees + utx_os = wallet.get_utx_os() else: - fees = endpoint.get_fee_estimates() + with ThreadPoolExecutor(max_workers=2) as executor: + f_fees = executor.submit(endpoint.get_fee_estimates) + f_utxos = executor.submit(wallet.get_utx_os) + fees = f_fees.result() + utx_os = f_utxos.result() print(f"\nUsing fee (override with -f): {fees} Satoshi\n") try: - wallet.prepare(self.data.address, amount, fees) + wallet.prepare(self.data.address, amount, fees, utx_os=utx_os) signatures = Btc._sign(card, derivation, wallet.data_hash) message = wallet.send(self.data.address, amount, signatures) diff --git a/cryptnox_cli/command/card/info.py b/cryptnox_cli/command/card/info.py index 399d942..3b7fb47 100644 --- a/cryptnox_cli/command/card/info.py +++ b/cryptnox_cli/command/card/info.py @@ -2,6 +2,7 @@ """ Module containing command for getting information about the card """ +from concurrent.futures import ThreadPoolExecutor from typing import List, Dict import cryptnox_sdk_py @@ -41,18 +42,29 @@ def __init__(self, data: Namespace, cards: CardManager = None): @staticmethod def execute(card) -> int: check(card) - print("Gathering information from the network...") - eth_info = Info._get_eth_info(card) - Info._print_info_table([ - Info._get_btc_info(card), - eth_info, - Info._get_xrp_info(card), - Info._get_bnb_info(card), - ]) + # Phase 1: fetch all public keys sequentially (card communication is not thread-safe) + btc_pubkey = Info._fetch_btc_pubkey(card) + eth_pubkey = Info._fetch_eth_pubkey(card) + xrp_pubkey = Info._fetch_xrp_pubkey(card) + # BNB uses the same derivation path and key as ETH + # Extract configs before spawning threads (card.user_data access is not thread-safe) config = get_configuration(card) + btc_config = config["btc"] + eth_config = config["eth"] + + # Phase 2: query all network balances in parallel + with ThreadPoolExecutor(max_workers=4) as executor: + f_btc = executor.submit(Info._get_btc_info, btc_config, btc_pubkey) + f_eth = executor.submit(Info._get_eth_info, eth_config, eth_pubkey) + f_xrp = executor.submit(Info._get_xrp_info, xrp_pubkey) + f_bnb = executor.submit(Info._get_bnb_info, eth_pubkey) + + eth_info = f_eth.result() + Info._print_info_table([f_btc.result(), eth_info, f_xrp.result(), f_bnb.result()]) + if not config["eth"]["api_key"] and config["eth"]["endpoint"] == "infura": print("\nTo use the Ethereum network with Infura. Go to https://infura.io. " "Register (free) and get an API key. Set the API key with: eth config api_key") @@ -63,19 +75,48 @@ def execute(card) -> int: return 0 @staticmethod - def _get_btc_info(card) -> dict: + def _fetch_btc_pubkey(card): config = get_configuration(card)["btc"] try: derivation = cryptnox_sdk_py.Derivation[config["derivation"]].value except KeyError: - return {"name": "Bad derivation type"} - network = config.get("network", "testnet").lower() - endpoint = BlkHubApi(network) - + return None path = b"" if derivation == cryptnox_sdk_py.Derivation.CURRENT_KEY else BTCwallet.PATH - pubkey = card.get_public_key(derivation, path=path) + try: + return card.get_public_key(derivation, path=path) + except Exception: + return None - wallet = BTCwallet(pubkey, network, endpoint, card) + @staticmethod + def _fetch_eth_pubkey(card): + config = get_configuration(card)["eth"] + try: + derivation = cryptnox_sdk_py.Derivation[config["derivation"]].value + except KeyError: + return None + path = "" if derivation == cryptnox_sdk_py.Derivation.CURRENT_KEY else eth.Api.PATH + try: + return card.get_public_key(derivation, path=path, compressed=False) + except Exception: + return None + + @staticmethod + def _fetch_xrp_pubkey(card): + try: + return card.get_public_key( + cryptnox_sdk_py.Derivation.DERIVE, + path=xrp_wallet.PATH + ) + except Exception: + return None + + @staticmethod + def _get_btc_info(config, pubkey) -> dict: + if pubkey is None: + return {"name": "BTC", "address": "Bad derivation type", "network": ""} + network = config.get("network", "testnet").lower() + endpoint = BlkHubApi(network) + wallet = BTCwallet(pubkey, network, endpoint, None) tabulate_data = { "name": "BTC", @@ -93,21 +134,16 @@ def _get_btc_info(card) -> dict: return tabulate_data @staticmethod - def _get_eth_info(card) -> dict: - config = get_configuration(card)["eth"] + def _get_eth_info(config, public_key) -> dict: + if public_key is None: + return {"name": "ETH", "address": "Bad derivation type", "network": ""} network = enums.EthNetwork[config.get("network", "infura").upper()] - try: - derivation = cryptnox_sdk_py.Derivation[config["derivation"]].value - except KeyError: - return {"name": "Bad derivation type"} try: api = eth.Api(config["endpoint"], network, config["api_key"]) except ValueError as error: print(error) return {} - path = "" if derivation == cryptnox_sdk_py.Derivation.CURRENT_KEY else eth.Api.PATH - public_key = card.get_public_key(derivation, path=path, compressed=False) address = eth.checksum_address(public_key) tabulate_data = { @@ -125,13 +161,12 @@ def _get_eth_info(card) -> dict: return tabulate_data @staticmethod - def _get_xrp_info(card) -> dict: + def _get_xrp_info(pubkey) -> dict: tabulate_data = {"name": "XRP", "network": "mainnet", "balance": "--"} + if pubkey is None: + tabulate_data["address"] = "Error" + return tabulate_data try: - pubkey = card.get_public_key( - cryptnox_sdk_py.Derivation.DERIVE, - path=xrp_wallet.PATH - ) tabulate_data["address"] = xrp_wallet.address(pubkey) except Exception as error: print(f"There's an issue in retrieving XRP address: {error}") @@ -148,19 +183,14 @@ def _get_xrp_info(card) -> dict: return tabulate_data @staticmethod - def _get_bnb_info(card) -> dict: + def _get_bnb_info(public_key) -> dict: # BNB on Binance Smart Chain (BSC) is EVM-compatible: same secp256k1 # curve, same derivation path, and same address format as Ethereum. - config = get_configuration(card)["eth"] - try: - derivation = cryptnox_sdk_py.Derivation[config["derivation"]].value - except KeyError: + if public_key is None: return {"name": "BNB", "address": "Bad derivation type", "network": "BSC mainnet"} tabulate_data = {"name": "BNB", "network": "BSC mainnet", "balance": "--"} try: - path = "" if derivation == cryptnox_sdk_py.Derivation.CURRENT_KEY else eth.Api.PATH - public_key = card.get_public_key(derivation, path=path, compressed=False) tabulate_data["address"] = eth.checksum_address(public_key) except Exception as error: print(f"There's an issue in retrieving BNB address: {error}") diff --git a/cryptnox_cli/command/options/options.py b/cryptnox_cli/command/options/options.py index 8dca59c..fbbd5c8 100644 --- a/cryptnox_cli/command/options/options.py +++ b/cryptnox_cli/command/options/options.py @@ -85,7 +85,7 @@ def _validate(address: str) -> str: send_sub_parser.add_argument("address", type=_validate, help="Address where to send funds") send_sub_parser.add_argument("amount", type=_validate_decimal, help="Amount to send") - send_sub_parser.add_argument("-n", "--network", choices=["mainnet", "testnet"], + send_sub_parser.add_argument("-n", "--network", choices=["mainnet", "testnet", "testnet4"], help="Network to use for transaction") send_sub_parser.add_argument("-f", "--fees", type=int, help="Fees to pay for the transaction") diff --git a/cryptnox_cli/wallet/btc.py b/cryptnox_cli/wallet/btc.py index 436d129..8d97e03 100644 --- a/cryptnox_cli/wallet/btc.py +++ b/cryptnox_cli/wallet/btc.py @@ -30,6 +30,7 @@ class BtcNetworks(Enum): """ MAINNET = "mainnet" TESTNET = "testnet" + TESTNET4 = "testnet4" class BlockCypherApi: @@ -182,6 +183,8 @@ def get_api(network: str) -> str: return "https://blkhub.net/api/" if network.lower() == "testnet": return "https://blockstream.info/testnet/api/" + if network.lower() == "testnet4": + return "https://mempool.space/testnet4/api/" raise Exception("Unknown BC network name") def _validate_endpoint(self, endpoint: str) -> str: @@ -216,12 +219,12 @@ def get_data(self, endpoint: str, params: Dict = None, data: bytes = None) \ params_enc = urllib.parse.urlencode(parameters) try: # Construct full URL and validate it stays within expected domain - full_url = self.url + endpoint + "?" + params_enc + full_url = self.url + endpoint + ("?" + params_enc if params_enc else "") parsed = urlparse(full_url) if not parsed.hostname or not any( domain in parsed.hostname - for domain in ['blkhub.net', 'blockstream.info']): - raise ValueError("Invalid URL: must be blkhub.net or blockstream.info domain") + for domain in ['blkhub.net', 'blockstream.info', 'mempool.space']): + raise ValueError("Invalid URL: must be blkhub.net, blockstream.info, or mempool.space domain") req = urllib.request.Request( full_url, @@ -342,7 +345,7 @@ def __init__(self, pubkey: str, coin_type: str, api, addr_header = 0x00 self.testnet = False coin_type = coin_type.lower() - if coin_type == "testnet": + if coin_type in ("testnet", "testnet4"): addr_header = 0x6F self.testnet = True self.pubkey = pubkey @@ -374,20 +377,22 @@ def get_balance(self) -> float: def get_fee_estimate(self): return self.api.get_fee_estimate() - def prepare(self, to_addr: str, payment_value: float, fee: float) \ - -> Union[float, int]: + def prepare(self, to_addr: str, payment_value: float, fee: float, + utx_os: List = None) -> Union[float, int]: """ :param to_addr: str :param payment_value: float :param fee: float + :param utx_os: pre-fetched UTXOs (fetched in parallel with fee estimate) :return: Union[float, int] """ self.fee = fee if not test_addr(to_addr): raise Exception("Bad address format.") - utx_os = self.get_utx_os() + if utx_os is None: + utx_os = self.get_utx_os() balance = self.balance_fm_utxos(utx_os) self.balance = balance / 10.0 ** 8 max_spendable = balance - fee