Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Build and deploy documentation
permissions:
contents: read
contents: write

on:
push:
Expand Down Expand Up @@ -46,11 +46,12 @@ jobs:
sphinx-build -M latexpdf docs docs/_build
cp docs/_build/latex/cryptnox-cli.pdf docs/_build/html/cryptnox-cli.pdf

# 5. Deploy HTML documentation to GitHub Pages
# 5. Deploy HTML documentation to GitHub Pages (only on push to main)
- name: Deploy to GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.DOCS_DEPLOY_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: docs/_build/html
publish_branch: gh-pages
allow_empty_commit: false
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:

permissions:
contents: read
actions: read # Required by codeql-action/upload-sarif to read workflow run info
security-events: write # Required for SARIF upload
pull-requests: write # For PR comments

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
__pycache__
*.egg-info/
*.pem
build
dist
Expand Down
3 changes: 2 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ base58 = "*"
boto3 = "*"
ecdsa = ">=0.19.2"
colander = "*"
cryptnox-sdk-py = ">=1.0.2"
cryptnox-sdk-py = ">=1.0.5"
lazy-import = "*"
pytz = "*"
requests = ">=2.33.0"
solders = ">=0.27.0"
tabulate = "*"
stdiomask = "*"
web3 = ">=7.15.0"
Expand Down
60 changes: 58 additions & 2 deletions cryptnox_cli/command/card/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Module containing command for getting information about the card
"""
from concurrent.futures import ThreadPoolExecutor
from decimal import Decimal
from typing import List, Dict

import cryptnox_sdk_py
Expand All @@ -23,12 +24,14 @@
from ...wallet import eth
from ...wallet.btc import BTCwallet, BlkHubApi
from ...wallet import xrp as xrp_wallet
from ...wallet import solana as solana_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
from wallet import solana as solana_wallet

__all__ = ['Cards']

Expand All @@ -48,22 +51,26 @@ def execute(card) -> int:
btc_pubkey = Info._fetch_btc_pubkey(card)
eth_pubkey = Info._fetch_eth_pubkey(card)
xrp_pubkey = Info._fetch_xrp_pubkey(card)
solana_pubkey = Info._fetch_solana_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"]
solana_config = config["solana"]

# Phase 2: query all network balances in parallel
with ThreadPoolExecutor(max_workers=4) as executor:
with ThreadPoolExecutor(max_workers=5) 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)
f_sol = executor.submit(Info._get_solana_info, solana_config, solana_pubkey)

eth_info = f_eth.result()
Info._print_info_table([f_btc.result(), eth_info, f_xrp.result(), f_bnb.result()])
Info._print_info_table([f_btc.result(), eth_info, f_xrp.result(), f_bnb.result(),
f_sol.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. "
Expand Down Expand Up @@ -110,6 +117,24 @@ def _fetch_xrp_pubkey(card):
except Exception:
return None

@staticmethod
def _fetch_solana_pubkey(card):
# None -> pre-2.0 applet (SDK's version guard rejects Ed25519).
# "" -> any other derivation/communication error.
# These are distinguished so the info table doesn't mislabel a network
# blip as "Requires applet v2.0".
try:
return card.get_public_key(
cryptnox_sdk_py.Derivation.DERIVE,
key_type=cryptnox_sdk_py.KeyType.ED25519,
path=solana_wallet.PATH,
compressed=False
)
except cryptnox_sdk_py.exceptions.AppletVersionException:
return None
except Exception:
return ""

@staticmethod
def _get_btc_info(config, pubkey) -> dict:
if pubkey is None:
Expand Down Expand Up @@ -182,6 +207,37 @@ def _get_xrp_info(pubkey) -> dict:

return tabulate_data

@staticmethod
def _get_solana_info(config, pubkey) -> dict:
network = config.get("network", "mainnet").lower()
tabulate_data = {"name": "SOL", "network": network, "balance": "--"}

# None -> pre-2.0 applet (no Ed25519); "" -> derivation/communication error.
if pubkey is None:
tabulate_data["address"] = "Requires applet v2.0"
return tabulate_data
if not pubkey:
tabulate_data["address"] = "Error"
return tabulate_data

try:
tabulate_data["address"] = solana_wallet.address(pubkey)
except Exception as error:
print(f"There's an issue in retrieving Solana address: {error}")
tabulate_data["address"] = "Error"
return tabulate_data

try:
api = solana_wallet.SolanaApi(network, config.get("endpoint", ""))
balance_lamports = api.get_balance(tabulate_data["address"])
balance_sol = Decimal(balance_lamports) / solana_wallet.LAMPORTS_PER_SOL
tabulate_data["balance"] = f"{balance_sol:f} SOL"
except Exception as error:
print(f"There's an issue in retrieving Solana balance: {error}")
tabulate_data["balance"] = "Network issue"

return tabulate_data

@staticmethod
def _get_bnb_info(public_key) -> dict:
# BNB on Binance Smart Chain (BSC) is EVM-compatible: same secp256k1
Expand Down
2 changes: 1 addition & 1 deletion cryptnox_cli/command/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def command(data: Namespace, cards: CardManager = None) -> Command:
# Dynamically import all command modules to register them with Command.__subclasses__()
command_modules = [
'btc', 'card_configuration', 'change_pin', 'change_puk', 'config',
'eth', 'history', 'info', 'initialize', 'seed', 'cards', 'server',
'eth', 'history', 'info', 'initialize', 'seed', 'solana', 'cards', 'server',
'reset', 'unlock_pin', 'user_key', 'transfer', 'get_xpub',
'get_clearpubkey', 'decrypt', 'manufacturer_certificate'
]
Expand Down
14 changes: 10 additions & 4 deletions cryptnox_cli/command/helper/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from wallet.validators import ValidationError
from wallet.btc import BlkHubApi, BtcValidator
from wallet.eth import EthValidator
from wallet.solana import SolanaValidator
except ImportError:
from ...config import (
get_configuration,
Expand All @@ -24,8 +25,9 @@
from ...wallet.validators import ValidationError
from ...wallet.btc import BlkHubApi, BtcValidator
from ...wallet.eth import EthValidator
from ...wallet.solana import SolanaValidator

__all__ = ["BtcValidator", "EthValidator"]
__all__ = ["BtcValidator", "EthValidator", "SolanaValidator"]


def add_config_sub_parser(sub_parser, crypto_currency: str) -> None:
Expand Down Expand Up @@ -122,11 +124,14 @@ def print_key_config(card: cryptnox_sdk_py.Card, section: str, key: str) -> int:
"""
config = get_configuration(card)
try:
old_key = key
key = "network" if key == "endpoint" and section != "eosio" else key
# btc/eth expose "endpoint" as a read-only value derived from "network".
# eosio and solana store a real, editable "endpoint" field, so don't
# remap their "endpoint" key to "network" (that printed a blank line).
_derived_endpoint = key == "endpoint" and section not in ("eosio", "solana")
key = "network" if _derived_endpoint else key
value = config[section][key]
endpoint = find_endpoint(section, key, value, " - YOU CAN'T EDIT THIS")
if old_key == "endpoint" and section != "eosio":
if _derived_endpoint:
print(endpoint)
else:
print(f"{key}: {value}")
Expand All @@ -148,6 +153,7 @@ def find_endpoint(section: str, key: str, value: str, append: str = "") -> str:
_VALIDATORS = {
"btc": BtcValidator,
"eth": EthValidator,
"solana": SolanaValidator,
}


Expand Down
7 changes: 3 additions & 4 deletions cryptnox_cli/command/helper/helper_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def exception(self):
def sign(card: cryptnox_sdk_py.Card, message: bytes,
derivation: cryptnox_sdk_py.Derivation = cryptnox_sdk_py.Derivation.CURRENT_KEY,
key_type: cryptnox_sdk_py.KeyType = cryptnox_sdk_py.KeyType.K1, path: str = "",
filter_eos: bool = False, pin_code: str = "") -> bytes:
pin_code: str = "") -> bytes:
"""
Open the card with a user key or PIN code and sign the given message in the given card

Expand All @@ -70,7 +70,6 @@ def sign(card: cryptnox_sdk_py.Card, message: bytes,
:param cryptnox_sdk_py.Derivation derivation: Derivation to use when signing
:param cryptnox_sdk_py.KeyType key_type: Key type to use when signing
:param str path: Path to use for signature generation
:param bool filter_eos: Filter signature to be compatible with eos requirements
:param str pin_code: If PIN code is given use it instead of asking for it

:return: Signature of the message generated in the card
Expand All @@ -79,12 +78,12 @@ def sign(card: cryptnox_sdk_py.Card, message: bytes,
signature = None

if user_keys.authenticate(card, message):
signature = card.sign(message, derivation, key_type, path, filter_eos=filter_eos)
signature = card.sign(message, derivation, key_type, path)

if not signature:
if not pin_code:
pin_code = security.check_pin_code(card)
signature = card.sign(message, derivation, key_type, path, pin_code, filter_eos)
signature = card.sign(message, derivation, key_type, path, pin_code)

if not signature:
raise ValueError("Error in getting the signature")
Expand Down
2 changes: 2 additions & 0 deletions cryptnox_cli/command/options/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import argparse

from . import eth
from . import solana
from .common import (
add_config_sub_parser,
add_pin_option
Expand Down Expand Up @@ -39,6 +40,7 @@ def add(parser, interactive: bool = False):

_btc_options(subparsers, interactive)
eth.options(subparsers, interactive)
solana.options(subparsers, interactive)
_transfer(subparsers, interactive)
_info_options(subparsers, interactive)
_history_options(subparsers, interactive)
Expand Down
78 changes: 78 additions & 0 deletions cryptnox_cli/command/options/solana.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
"""
Module for Solana-specific command-line argument parsing and validation,
including the send action, recipient address validation and network selection.
"""

from decimal import Decimal, InvalidOperation

import argparse
import base58

from .common import (
add_config_sub_parser,
add_pin_option
)

try:
import enums
except ImportError:
from ... import enums


# Sane upper bound: ~600M SOL exist in total; 1e9 SOL is well above any real
# transfer and keeps lamports (amount * 1e9) inside solders' u64 field.
_MAX_SOL = Decimal(10) ** 9


def _validate_decimal(value: str) -> Decimal:
try:
amount = Decimal(value)
except InvalidOperation:
raise argparse.ArgumentTypeError(
f"Invalid amount: '{value}'. Please provide a valid number"
)
if not amount.is_finite():
raise argparse.ArgumentTypeError("Amount must be a finite number")
if amount <= 0:
raise argparse.ArgumentTypeError("Amount must be greater than 0")
if amount > _MAX_SOL:
raise argparse.ArgumentTypeError(f"Amount must not exceed {_MAX_SOL:f} SOL")
return amount


def _network_choices():
return [e.name.lower() for e in enums.SolanaNetwork]


def _validate(address: str) -> str:
try:
decoded = base58.b58decode(address)
except ValueError:
raise argparse.ArgumentTypeError("Not a valid Solana address")

if len(decoded) != 32:
raise argparse.ArgumentTypeError("Not a valid Solana address")

return address


def _add_send(subparsers):
sub_parser = subparsers.add_parser("send", help="Simple command to send native SOL")
sub_parser.add_argument("address", type=_validate, help="Address where to send funds")
sub_parser.add_argument("amount", type=_validate_decimal, help="Amount to send")
sub_parser.add_argument("-n", "--network", choices=_network_choices(),
help="Network to use for transaction")


def options(subparsers, pin_option: bool):
solana_sub_parser = subparsers.add_parser(enums.Command.SOLANA.value,
help="Solana subcommands")

if pin_option:
add_pin_option(solana_sub_parser)

action_sub_parser = solana_sub_parser.add_subparsers(dest="solana_action", required=True)

_add_send(action_sub_parser)
add_config_sub_parser(action_sub_parser, "Solana")
Loading
Loading