From 25656c56f04007efea1f81ca0cbbeddcc8da7d65 Mon Sep 17 00:00:00 2001 From: YanNaingWinn Date: Fri, 27 Mar 2026 02:47:00 +0700 Subject: [PATCH 1/3] Added Bitcoin testnet4 support and parallel network queries --- cryptnox_cli/command/btc.py | 11 ++- cryptnox_cli/command/card/info.py | 97 ++++++++++++++++--------- cryptnox_cli/command/options/options.py | 2 +- cryptnox_cli/wallet/btc.py | 19 +++-- 4 files changed, 86 insertions(+), 43 deletions(-) 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..e57f9ae 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,16 +42,26 @@ 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), - ]) + # Warm config cache before spawning threads (avoids card.user_data access in threads) + get_configuration(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 + + # Phase 2: query all network balances in parallel + with ThreadPoolExecutor(max_workers=4) as executor: + f_btc = executor.submit(Info._get_btc_info, card, btc_pubkey) + f_eth = executor.submit(Info._get_eth_info, card, 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()]) config = get_configuration(card) if not config["eth"]["api_key"] and config["eth"]["endpoint"] == "infura": @@ -63,18 +74,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 + + @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(card, pubkey) -> dict: + if pubkey is None: + return {"name": "BTC", "address": "Bad derivation type", "network": ""} + config = get_configuration(card)["btc"] + network = config.get("network", "testnet").lower() + endpoint = BlkHubApi(network) wallet = BTCwallet(pubkey, network, endpoint, card) tabulate_data = { @@ -93,21 +134,17 @@ def _get_btc_info(card) -> dict: return tabulate_data @staticmethod - def _get_eth_info(card) -> dict: + def _get_eth_info(card, public_key) -> dict: + if public_key is None: + return {"name": "ETH", "address": "Bad derivation type", "network": ""} config = get_configuration(card)["eth"] 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 +162,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 +184,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 From 6d5d4c517516357173db28d988b59a49b74e6102 Mon Sep 17 00:00:00 2001 From: YanNaingWinn Date: Thu, 2 Apr 2026 03:39:38 +0700 Subject: [PATCH 2/3] Fix thread-safety: pre-fetch card config before ThreadPoolExecutor --- cryptnox_cli/command/card/info.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/cryptnox_cli/command/card/info.py b/cryptnox_cli/command/card/info.py index e57f9ae..60e48c8 100644 --- a/cryptnox_cli/command/card/info.py +++ b/cryptnox_cli/command/card/info.py @@ -44,26 +44,27 @@ def execute(card) -> int: check(card) print("Gathering information from the network...") - # Warm config cache before spawning threads (avoids card.user_data access in threads) - get_configuration(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, card, btc_pubkey) - f_eth = executor.submit(Info._get_eth_info, card, eth_pubkey) + 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()]) - config = get_configuration(card) 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") @@ -110,10 +111,9 @@ def _fetch_xrp_pubkey(card): return None @staticmethod - def _get_btc_info(card, pubkey) -> dict: + def _get_btc_info(config, pubkey) -> dict: if pubkey is None: return {"name": "BTC", "address": "Bad derivation type", "network": ""} - config = get_configuration(card)["btc"] network = config.get("network", "testnet").lower() endpoint = BlkHubApi(network) wallet = BTCwallet(pubkey, network, endpoint, card) @@ -134,10 +134,9 @@ def _get_btc_info(card, pubkey) -> dict: return tabulate_data @staticmethod - def _get_eth_info(card, public_key) -> dict: + def _get_eth_info(config, public_key) -> dict: if public_key is None: return {"name": "ETH", "address": "Bad derivation type", "network": ""} - config = get_configuration(card)["eth"] network = enums.EthNetwork[config.get("network", "infura").upper()] try: api = eth.Api(config["endpoint"], network, config["api_key"]) From 31f9503de827226033ff388d040abc8042fab4a8 Mon Sep 17 00:00:00 2001 From: YanNaingWinn Date: Thu, 2 Apr 2026 04:12:07 +0700 Subject: [PATCH 3/3] Resolved flake8 error --- cryptnox_cli/command/card/info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptnox_cli/command/card/info.py b/cryptnox_cli/command/card/info.py index 60e48c8..3b7fb47 100644 --- a/cryptnox_cli/command/card/info.py +++ b/cryptnox_cli/command/card/info.py @@ -116,7 +116,7 @@ def _get_btc_info(config, pubkey) -> dict: return {"name": "BTC", "address": "Bad derivation type", "network": ""} network = config.get("network", "testnet").lower() endpoint = BlkHubApi(network) - wallet = BTCwallet(pubkey, network, endpoint, card) + wallet = BTCwallet(pubkey, network, endpoint, None) tabulate_data = { "name": "BTC",