From ff751b022598f069df731c8c1fc3858ace15d12a Mon Sep 17 00:00:00 2001 From: YanNaingWinn Date: Tue, 24 Feb 2026 23:53:42 +0700 Subject: [PATCH 1/3] Implemented XRP and BNB addresses display and extended BTC send to accept bech32 (SegWit) addresses --- cryptnox_cli/command/card/info.py | 62 +++++++++++++++++++- cryptnox_cli/command/options/options.py | 3 +- cryptnox_cli/wallet/btc.py | 2 + cryptnox_cli/wallet/xrp.py | 76 +++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 cryptnox_cli/wallet/xrp.py diff --git a/cryptnox_cli/command/card/info.py b/cryptnox_cli/command/card/info.py index 352e5c6..399d942 100644 --- a/cryptnox_cli/command/card/info.py +++ b/cryptnox_cli/command/card/info.py @@ -21,11 +21,13 @@ from ...config import get_configuration from ...wallet import eth from ...wallet.btc import BTCwallet, BlkHubApi + from ...wallet import xrp as xrp_wallet except ImportError: import enums from config import get_configuration from wallet import eth from wallet.btc import BTCwallet, BlkHubApi + from wallet import xrp as xrp_wallet __all__ = ['Cards'] @@ -43,7 +45,12 @@ def execute(card) -> int: 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._print_info_table([ + Info._get_btc_info(card), + eth_info, + Info._get_xrp_info(card), + Info._get_bnb_info(card), + ]) config = get_configuration(card) if not config["eth"]["api_key"] and config["eth"]["endpoint"] == "infura": @@ -117,6 +124,59 @@ def _get_eth_info(card) -> dict: return tabulate_data + @staticmethod + def _get_xrp_info(card) -> dict: + tabulate_data = {"name": "XRP", "network": "mainnet", "balance": "--"} + 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}") + tabulate_data["address"] = "Error" + return tabulate_data + + try: + balance = xrp_wallet.get_balance(tabulate_data["address"]) + tabulate_data["balance"] = f"{balance} XRP" + except Exception as error: + print(f"There's an issue in retrieving XRP balance: {error}") + tabulate_data["balance"] = "Network issue" + + return tabulate_data + + @staticmethod + def _get_bnb_info(card) -> 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: + 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}") + tabulate_data["address"] = "Error" + return tabulate_data + + try: + w3 = web3.Web3(web3.Web3.HTTPProvider("https://bsc-dataseed.binance.org/")) + balance_wei = w3.eth.get_balance(tabulate_data["address"]) + tabulate_data["balance"] = f"{web3.Web3.from_wei(balance_wei, 'ether')} BNB" + except Exception as error: + print(f"There's an issue in retrieving BNB balance: {error}") + tabulate_data["balance"] = "Network issue" + + return tabulate_data + @staticmethod def _print_info_table(info: List[Dict]) -> None: print("\n") diff --git a/cryptnox_cli/command/options/options.py b/cryptnox_cli/command/options/options.py index 9dbc461..8dca59c 100644 --- a/cryptnox_cli/command/options/options.py +++ b/cryptnox_cli/command/options/options.py @@ -75,7 +75,8 @@ def _btc_options(subparsers, interactive_mode): def add_send(sub_parser): def _validate(address: str) -> str: if re.match('^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$', address) or \ - re.match('^[2nm][a-km-zA-HJ-NP-Z1-9]{25,34}$', address): + re.match('^[2nm][a-km-zA-HJ-NP-Z1-9]{25,34}$', address) or \ + re.match('^(bc1|tb1)[a-zA-HJ-NP-Z0-9]{25,62}$', address): return address raise argparse.ArgumentTypeError("Not a valid BTC address") diff --git a/cryptnox_cli/wallet/btc.py b/cryptnox_cli/wallet/btc.py index a17cc39..436d129 100644 --- a/cryptnox_cli/wallet/btc.py +++ b/cryptnox_cli/wallet/btc.py @@ -319,6 +319,8 @@ def test_addr(btc_addr: str): if btc_addr.startswith("n") or btc_addr.startswith( "m") or btc_addr.startswith("2"): return re.match('^[2nm][a-km-zA-HJ-NP-Z1-9]{25,34}$', btc_addr) + if btc_addr.startswith("bc1") or btc_addr.startswith("tb1"): + return re.match('^(bc1|tb1)[a-zA-HJ-NP-Z0-9]{25,62}$', btc_addr) return False diff --git a/cryptnox_cli/wallet/xrp.py b/cryptnox_cli/wallet/xrp.py new file mode 100644 index 0000000..ce05db4 --- /dev/null +++ b/cryptnox_cli/wallet/xrp.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" +XRP address derivation and balance lookup from secp256k1 public key. + +XRP uses the same secp256k1 curve as Bitcoin. The address is derived by: +1. SHA-256 hash of the compressed public key +2. RIPEMD-160 of the SHA-256 result (Account ID) +3. Base58Check encoding with version byte 0x00 using the XRP alphabet +""" +import hashlib + +import base58 +import requests + +PATH = "m/44'/144'/0'/0/0" + +# XRP uses a different Base58 alphabet than Bitcoin +XRP_ALPHABET = b'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz' + +MAINNET_RPC = "https://s1.ripple.com:51234/" + +# 1 XRP = 1 000 000 drops +_DROPS_PER_XRP = 1_000_000 + + +def address(public_key_hex: str) -> str: + """ + Derive an XRP classic address (r-address) from a compressed secp256k1 public key. + + :param str public_key_hex: Compressed public key as a hex string (33 bytes / 66 hex chars) + :return: XRP address starting with 'r' + :rtype: str + """ + public_key_bytes = bytes.fromhex(public_key_hex) + + sha256_hash = hashlib.sha256(public_key_bytes).digest() + + ripemd160 = hashlib.new('ripemd160') + ripemd160.update(sha256_hash) + account_id = ripemd160.digest() + + # Version byte 0x00 for XRP classic addresses + return base58.b58encode_check(b'\x00' + account_id, alphabet=XRP_ALPHABET).decode() + + +def get_balance(xrp_address: str) -> float: + """ + Return the XRP balance for *xrp_address* in XRP (not drops). + + Returns 0.0 for accounts that exist on-chain but have not yet been funded + (actNotFound). Raises an exception for any other network or protocol error. + + :param str xrp_address: Classic r-address (e.g. 'rHb9CJA...') + :return: Balance in XRP + :rtype: float + :raises requests.RequestException: On HTTP / connection failures + :raises ValueError: On unexpected ledger error responses + """ + payload = { + "method": "account_info", + "params": [{"account": xrp_address, "ledger_index": "current"}], + } + response = requests.post(MAINNET_RPC, json=payload, timeout=10) + response.raise_for_status() + + result = response.json().get("result", {}) + + # Account exists but has never been funded – treat as zero balance + if result.get("error") == "actNotFound": + return 0.0 + + if result.get("status") == "error": + raise ValueError(result.get("error_message", "XRP network error")) + + drops = int(result["account_data"]["Balance"]) + return drops / _DROPS_PER_XRP From ad934f2efc2945609c8b07e258ffe846bc530190 Mon Sep 17 00:00:00 2001 From: YanNaingWinn Date: Fri, 6 Mar 2026 20:08:28 +0700 Subject: [PATCH 2/3] Add CodeQL workflow for security scanning Required by branch protection ruleset to allow PR merge to main. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/codeql.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..bae9da3 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,34 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '0 0 * * 1' + +permissions: + contents: read + security-events: write + actions: read + +jobs: + analyze: + name: Analyze (python) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: python + queries: security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: CodeQL From 2e2dcaf5a913c84d313ccfa577a8b3ce41777e5e Mon Sep 17 00:00:00 2001 From: YanNaingWinn Date: Fri, 6 Mar 2026 20:12:38 +0700 Subject: [PATCH 3/3] Remove path filter from osv-scanner workflow Required to run on all PRs so branch protection code scanning check is satisfied even when requirements files are unchanged. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/osv-scanner.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml index bbca8cf..cab0ec8 100644 --- a/.github/workflows/osv-scanner.yml +++ b/.github/workflows/osv-scanner.yml @@ -9,9 +9,6 @@ on: - '.github/workflows/osv-scanner.yml' pull_request: branches: [main] - paths: - - 'requirements.txt' - - 'dev-requirements.txt' schedule: # Run weekly on Monday at 00:00 UTC to catch new CVEs - cron: '0 0 * * 1'