chore(common): add type hints to some coin tools scripts

Type-checking tools are still complaining at some places about inconsistencies.
It would be too much effort to make them completely happy.
pull/2303/head
grdddj 2 years ago committed by Jiří Musil
parent 5671bd037b
commit 519f79f9eb

@ -1,10 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import json import json
import logging import logging
import os import os
import re import re
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Iterable, Iterator, Literal, TypedDict, cast
try: try:
import requests import requests
@ -21,7 +24,125 @@ else:
DEFS_DIR = ROOT / "defs" DEFS_DIR = ROOT / "defs"
def load_json(*path): class SupportItemBool(TypedDict):
supported: dict[str, bool]
unsupported: dict[str, bool]
class SupportItemVersion(TypedDict):
supported: dict[str, str]
unsupported: dict[str, str]
class SupportData(TypedDict):
connect: SupportItemBool
suite: SupportItemBool
trezor1: SupportItemVersion
trezor2: SupportItemVersion
class SupportInfoItem(TypedDict):
connect: bool
suite: bool
trezor1: Literal[False] | str
trezor2: Literal[False] | str
SupportInfo = dict[str, SupportInfoItem]
class Coin(TypedDict):
# Necessary fields for BTC - from BTC_CHECKS
coin_name: str
coin_shortcut: str
coin_label: str
website: str
github: str
maintainer: str
curve_name: str
address_type: int
address_type_p2sh: int
maxfee_kb: int
minfee_kb: int
hash_genesis_block: str
xprv_magic: int
xpub_magic: int
xpub_magic_segwit_p2sh: int
xpub_magic_segwit_native: int
slip44: int
segwit: bool
decred: bool
fork_id: int
force_bip143: bool
default_fee_b: dict[str, int]
dust_limit: int
blocktime_seconds: int
signed_message_header: str
uri_prefix: str
min_address_length: int
max_address_length: int
bech32_prefix: str
cashaddr_prefix: str
# Other fields optionally coming from JSON
links: dict[str, str]
wallet: dict[str, str]
curve: str
decimals: int
# Mandatory fields added later in coin.update()
name: str
shortcut: str
key: str
icon: str
# Special ETH fields
chain: str
chain_id: str
rskip60: bool
url: str
# Special erc20 fields
symbol: str
address: str
address_bytes: bytes
dup_key_nontoken: bool
deprecation: dict[str, str]
# Special NEM fields
ticker: str
# Fields that are being created
unsupported: bool
duplicate: bool
support: SupportInfoItem
# Backend-oriented fields
blockchain_link: dict[str, Any]
blockbook: list[str]
bitcore: list[str]
Coins = list[Coin]
CoinBuckets = dict[str, Coins]
class FidoApp(TypedDict):
name: str
webauthn: list[str]
u2f: list[dict[str, str]]
use_sign_count: bool
use_self_attestation: bool
no_icon: bool
key: str
icon: str
FidoApps = list[FidoApp]
def load_json(*path: str | Path) -> Any:
"""Convenience function to load a JSON file from DEFS_DIR.""" """Convenience function to load a JSON file from DEFS_DIR."""
if len(path) == 1 and isinstance(path[0], Path): if len(path) == 1 and isinstance(path[0], Path):
file = path[0] file = path[0]
@ -34,7 +155,7 @@ def load_json(*path):
# ====== CoinsInfo ====== # ====== CoinsInfo ======
class CoinsInfo(dict): class CoinsInfo(dict[str, Coins]):
"""Collection of information about all known kinds of coins. """Collection of information about all known kinds of coins.
It contains the following lists: It contains the following lists:
@ -47,13 +168,13 @@ class CoinsInfo(dict):
Accessible as a dict or by attribute: `info["misc"] == info.misc` Accessible as a dict or by attribute: `info["misc"] == info.misc`
""" """
def as_list(self): def as_list(self) -> Coins:
return sum(self.values(), []) return sum(self.values(), [])
def as_dict(self): def as_dict(self) -> dict[str, Coin]:
return {coin["key"]: coin for coin in self.as_list()} return {coin["key"]: coin for coin in self.as_list()}
def __getattr__(self, attr): def __getattr__(self, attr: str) -> Coins:
if attr in self: if attr in self:
return self[attr] return self[attr]
else: else:
@ -63,7 +184,14 @@ class CoinsInfo(dict):
# ====== coin validation ====== # ====== coin validation ======
def check_type(val, types, nullable=False, empty=False, regex=None, choice=None): def check_type(
val: Any,
types: type | tuple[type, ...],
nullable: bool = False,
empty: bool = False,
regex: str | None = None,
choice: list[str] | None = None,
) -> None:
# check nullable # check nullable
if val is None: if val is None:
if nullable: if nullable:
@ -85,6 +213,7 @@ def check_type(val, types, nullable=False, empty=False, regex=None, choice=None)
if regex is not None: if regex is not None:
if types is not str: if types is not str:
raise TypeError("Wrong type for regex check") raise TypeError("Wrong type for regex check")
assert isinstance(val, str)
if not re.search(regex, val): if not re.search(regex, val):
raise ValueError(f"Value does not match regex {regex}") raise ValueError(f"Value does not match regex {regex}")
@ -94,8 +223,10 @@ def check_type(val, types, nullable=False, empty=False, regex=None, choice=None)
raise ValueError(f"Value not allowed, use one of: {choice_str}") raise ValueError(f"Value not allowed, use one of: {choice_str}")
def check_key(key, types, optional=False, **kwargs): def check_key(
def do_check(coin): key: str, types: type | tuple[type, ...], optional: bool = False, **kwargs: Any
) -> Callable[[Coin], None]:
def do_check(coin: Coin) -> None:
if key not in coin: if key not in coin:
if optional: if optional:
return return
@ -152,15 +283,15 @@ BTC_CHECKS = [
] ]
def validate_btc(coin): def validate_btc(coin: Coin) -> list[str]:
errors = [] errors: list[str] = []
for check in BTC_CHECKS: for check in BTC_CHECKS:
try: try:
check(coin) check(coin)
except Exception as e: except Exception as e:
errors.append(str(e)) errors.append(str(e))
magics = [ magics: list[int] = [
coin[k] coin[k]
for k in ( for k in (
"xprv_magic", "xprv_magic",
@ -208,11 +339,11 @@ def validate_btc(coin):
# ======= Coin json loaders ======= # ======= Coin json loaders =======
def _load_btc_coins(): def _load_btc_coins() -> Coins:
"""Load btc-like coins from `bitcoin/*.json`""" """Load btc-like coins from `bitcoin/*.json`"""
coins = [] coins: Coins = []
for file in DEFS_DIR.glob("bitcoin/*.json"): for file in DEFS_DIR.glob("bitcoin/*.json"):
coin = load_json(file) coin: Coin = load_json(file)
coin.update( coin.update(
name=coin["coin_label"], name=coin["coin_label"],
shortcut=coin["coin_shortcut"], shortcut=coin["coin_shortcut"],
@ -224,10 +355,10 @@ def _load_btc_coins():
return coins return coins
def _load_ethereum_networks(): def _load_ethereum_networks() -> Coins:
"""Load ethereum networks from `ethereum/networks.json`""" """Load ethereum networks from `ethereum/networks.json`"""
chains_path = DEFS_DIR / "ethereum" / "chains" / "_data" / "chains" chains_path = DEFS_DIR / "ethereum" / "chains" / "_data" / "chains"
networks = [] networks: Coins = []
for chain in sorted( for chain in sorted(
chains_path.glob("eip155-*.json"), chains_path.glob("eip155-*.json"),
key=lambda x: int(x.stem.replace("eip155-", "")), key=lambda x: int(x.stem.replace("eip155-", "")),
@ -261,21 +392,21 @@ def _load_ethereum_networks():
url=chain_data["infoURL"], url=chain_data["infoURL"],
key=f"eth:{shortcut}", key=f"eth:{shortcut}",
) )
networks.append(network) networks.append(cast(Coin, network))
return networks return networks
def _load_erc20_tokens(): def _load_erc20_tokens() -> Coins:
"""Load ERC20 tokens from `ethereum/tokens` submodule.""" """Load ERC20 tokens from `ethereum/tokens` submodule."""
networks = _load_ethereum_networks() networks = _load_ethereum_networks()
tokens = [] tokens: Coins = []
for network in networks: for network in networks:
chain = network["chain"] chain = network["chain"]
chain_path = DEFS_DIR / "ethereum" / "tokens" / "tokens" / chain chain_path = DEFS_DIR / "ethereum" / "tokens" / "tokens" / chain
for file in sorted(chain_path.glob("*.json")): for file in sorted(chain_path.glob("*.json")):
token = load_json(file) token: Coin = load_json(file)
token.update( token.update(
chain=chain, chain=chain,
chain_id=network["chain_id"], chain_id=network["chain_id"],
@ -288,26 +419,26 @@ def _load_erc20_tokens():
return tokens return tokens
def _load_nem_mosaics(): def _load_nem_mosaics() -> Coins:
"""Loads NEM mosaics from `nem/nem_mosaics.json`""" """Loads NEM mosaics from `nem/nem_mosaics.json`"""
mosaics = load_json("nem/nem_mosaics.json") mosaics: Coins = load_json("nem/nem_mosaics.json")
for mosaic in mosaics: for mosaic in mosaics:
shortcut = mosaic["ticker"].strip() shortcut = mosaic["ticker"].strip()
mosaic.update(shortcut=shortcut, key=f"nem:{shortcut}") mosaic.update(shortcut=shortcut, key=f"nem:{shortcut}")
return mosaics return mosaics
def _load_misc(): def _load_misc() -> Coins:
"""Loads miscellaneous networks from `misc/misc.json`""" """Loads miscellaneous networks from `misc/misc.json`"""
others = load_json("misc/misc.json") others: Coins = load_json("misc/misc.json")
for other in others: for other in others:
other.update(key=f"misc:{other['shortcut']}") other.update(key=f"misc:{other['shortcut']}")
return others return others
def _load_fido_apps(): def _load_fido_apps() -> FidoApps:
"""Load FIDO apps from `fido/*.json`""" """Load FIDO apps from `fido/*.json`"""
apps = [] apps: FidoApps = []
for file in sorted(DEFS_DIR.glob("fido/*.json")): for file in sorted(DEFS_DIR.glob("fido/*.json")):
app_name = file.stem.lower() app_name = file.stem.lower()
app = load_json(file) app = load_json(file)
@ -335,28 +466,28 @@ MISSING_SUPPORT_MEANS_NO = ("connect", "suite")
VERSIONED_SUPPORT_INFO = ("trezor1", "trezor2") VERSIONED_SUPPORT_INFO = ("trezor1", "trezor2")
def get_support_data(): def get_support_data() -> SupportData:
"""Get raw support data from `support.json`.""" """Get raw support data from `support.json`."""
return load_json("support.json") return load_json("support.json")
def latest_releases(): def latest_releases() -> dict[str, Any]:
"""Get latest released firmware versions for Trezor 1 and 2""" """Get latest released firmware versions for Trezor 1 and 2"""
if not requests: if not requests:
raise RuntimeError("requests library is required for getting release info") raise RuntimeError("requests library is required for getting release info")
latest = {} latest: dict[str, Any] = {}
for v in ("1", "2"): for v in ("1", "2"):
releases = requests.get(RELEASES_URL.format(v)).json() releases = requests.get(RELEASES_URL.format(v)).json()
latest["trezor" + v] = max(tuple(r["version"]) for r in releases) latest["trezor" + v] = max(tuple(r["version"]) for r in releases)
return latest return latest
def is_token(coin): def is_token(coin: Coin) -> bool:
return coin["key"].startswith("erc20:") return coin["key"].startswith("erc20:")
def support_info_single(support_data, coin): def support_info_single(support_data: SupportData, coin: Coin) -> SupportInfoItem:
"""Extract a support dict from `support.json` data. """Extract a support dict from `support.json` data.
Returns a dict of support values for each "device", i.e., `support.json` Returns a dict of support values for each "device", i.e., `support.json`
@ -368,22 +499,23 @@ def support_info_single(support_data, coin):
(usually a version string, or `True` for connect/suite) (usually a version string, or `True` for connect/suite)
* if the coin doesn't have an entry, its support status is `None` * if the coin doesn't have an entry, its support status is `None`
""" """
support_info = {} support_info_item = {}
key = coin["key"] key = coin["key"]
for device, values in support_data.items(): for device, values in support_data.items():
assert isinstance(values, dict)
if key in values["unsupported"]: if key in values["unsupported"]:
support_value = False support_value: Any = False
elif key in values["supported"]: elif key in values["supported"]:
support_value = values["supported"][key] support_value = values["supported"][key]
elif device in MISSING_SUPPORT_MEANS_NO: elif device in MISSING_SUPPORT_MEANS_NO:
support_value = False support_value = False
else: else:
support_value = None support_value = None
support_info[device] = support_value support_info_item[device] = support_value
return support_info return cast(SupportInfoItem, support_info_item)
def support_info(coins): def support_info(coins: Iterable[Coin] | CoinsInfo | dict[str, Coin]) -> SupportInfo:
"""Generate Trezor support information. """Generate Trezor support information.
Takes a collection of coins and generates a support-info entry for each. Takes a collection of coins and generates a support-info entry for each.
@ -401,7 +533,7 @@ def support_info(coins):
coins = coins.values() coins = coins.values()
support_data = get_support_data() support_data = get_support_data()
support = {} support: SupportInfo = {}
for coin in coins: for coin in coins:
support[coin["key"]] = support_info_single(support_data, coin) support[coin["key"]] = support_info_single(support_data, coin)
@ -411,19 +543,19 @@ def support_info(coins):
# ====== data cleanup functions ====== # ====== data cleanup functions ======
def _ensure_mandatory_values(coins): def _ensure_mandatory_values(coins: Coins) -> None:
"""Checks that every coin has the mandatory fields: name, shortcut, key""" """Checks that every coin has the mandatory fields: name, shortcut, key"""
for coin in coins: for coin in coins:
if not all(coin.get(k) for k in ("name", "shortcut", "key")): if not all(coin.get(k) for k in ("name", "shortcut", "key")):
raise ValueError(coin) raise ValueError(coin)
def symbol_from_shortcut(shortcut): def symbol_from_shortcut(shortcut: str) -> tuple[str, str]:
symsplit = shortcut.split(" ", maxsplit=1) symsplit = shortcut.split(" ", maxsplit=1)
return symsplit[0], symsplit[1] if len(symsplit) > 1 else "" return symsplit[0], symsplit[1] if len(symsplit) > 1 else ""
def mark_duplicate_shortcuts(coins): def mark_duplicate_shortcuts(coins: Coins) -> CoinBuckets:
"""Finds coins with identical symbols and sets their `duplicate` field. """Finds coins with identical symbols and sets their `duplicate` field.
"Symbol" here means the first part of `shortcut` (separated by space), "Symbol" here means the first part of `shortcut` (separated by space),
@ -436,7 +568,7 @@ def mark_duplicate_shortcuts(coins):
Each coin in every bucket will have its "duplicate" property set to True, unless Each coin in every bucket will have its "duplicate" property set to True, unless
it's explicitly marked as `false` in `duplicity_overrides.json`. it's explicitly marked as `false` in `duplicity_overrides.json`.
""" """
dup_symbols = defaultdict(list) dup_symbols: CoinBuckets = defaultdict(list)
for coin in coins: for coin in coins:
symbol, _ = symbol_from_shortcut(coin["shortcut"].lower()) symbol, _ = symbol_from_shortcut(coin["shortcut"].lower())
@ -451,9 +583,9 @@ def mark_duplicate_shortcuts(coins):
return dup_symbols return dup_symbols
def apply_duplicity_overrides(coins): def apply_duplicity_overrides(coins: Coins) -> Coins:
overrides = load_json("duplicity_overrides.json") overrides = load_json("duplicity_overrides.json")
override_bucket = [] override_bucket: Coins = []
for coin in coins: for coin in coins:
override_value = overrides.get(coin["key"]) override_value = overrides.get(coin["key"])
if override_value is True: if override_value is True:
@ -464,7 +596,7 @@ def apply_duplicity_overrides(coins):
return override_bucket return override_bucket
def deduplicate_erc20(buckets, networks): def deduplicate_erc20(buckets: CoinBuckets, networks: Coins) -> None:
"""Apply further processing to ERC20 duplicate buckets. """Apply further processing to ERC20 duplicate buckets.
This function works on results of `mark_duplicate_shortcuts`. This function works on results of `mark_duplicate_shortcuts`.
@ -489,7 +621,7 @@ def deduplicate_erc20(buckets, networks):
testnet_networks = {n["chain"] for n in networks if "Testnet" in n["name"]} testnet_networks = {n["chain"] for n in networks if "Testnet" in n["name"]}
def clear_bucket(bucket): def clear_bucket(bucket: Coins) -> None:
# allow all coins, except those that are explicitly marked through overrides # allow all coins, except those that are explicitly marked through overrides
for coin in bucket: for coin in bucket:
coin["duplicate"] = False coin["duplicate"] = False
@ -531,8 +663,8 @@ def deduplicate_erc20(buckets, networks):
clear_bucket(bucket) clear_bucket(bucket)
def deduplicate_keys(all_coins): def deduplicate_keys(all_coins: Coins) -> None:
dups = defaultdict(list) dups: CoinBuckets = defaultdict(list)
for coin in all_coins: for coin in all_coins:
dups[coin["key"]].append(coin) dups[coin["key"]].append(coin)
@ -549,7 +681,7 @@ def deduplicate_keys(all_coins):
coin["dup_key_nontoken"] = True coin["dup_key_nontoken"] = True
def fill_blockchain_links(all_coins): def fill_blockchain_links(all_coins: CoinsInfo) -> None:
blockchain_links = load_json("blockchain_link.json") blockchain_links = load_json("blockchain_link.json")
for coins in all_coins.values(): for coins in all_coins.values():
for coin in coins: for coin in coins:
@ -561,14 +693,14 @@ def fill_blockchain_links(all_coins):
coin["blockbook"] = [] coin["blockbook"] = []
def _btc_sort_key(coin): def _btc_sort_key(coin: Coin) -> str:
if coin["name"] in ("Bitcoin", "Testnet", "Regtest"): if coin["name"] in ("Bitcoin", "Testnet", "Regtest"):
return "000000" + coin["name"] return "000000" + coin["name"]
else: else:
return coin["name"] return coin["name"]
def collect_coin_info(): def collect_coin_info() -> CoinsInfo:
"""Returns all definition as dict organized by coin type. """Returns all definition as dict organized by coin type.
`coins` for btc-like coins, `coins` for btc-like coins,
`eth` for ethereum networks, `eth` for ethereum networks,
@ -592,7 +724,7 @@ def collect_coin_info():
return all_coins return all_coins
def sort_coin_infos(all_coins): def sort_coin_infos(all_coins: CoinsInfo) -> None:
for k, coins in all_coins.items(): for k, coins in all_coins.items():
if k == "bitcoin": if k == "bitcoin":
coins.sort(key=_btc_sort_key) coins.sort(key=_btc_sort_key)
@ -606,7 +738,7 @@ def sort_coin_infos(all_coins):
coins.sort(key=lambda c: c["key"].upper()) coins.sort(key=lambda c: c["key"].upper())
def coin_info_with_duplicates(): def coin_info_with_duplicates() -> tuple[CoinsInfo, CoinBuckets]:
"""Collects coin info, detects duplicates but does not remove them. """Collects coin info, detects duplicates but does not remove them.
Returns the CoinsInfo object and duplicate buckets. Returns the CoinsInfo object and duplicate buckets.
@ -626,7 +758,7 @@ def coin_info_with_duplicates():
return all_coins, buckets return all_coins, buckets
def coin_info(): def coin_info() -> CoinsInfo:
"""Collects coin info, fills out support info and returns the result. """Collects coin info, fills out support info and returns the result.
Does not auto-delete duplicates. This should now be based on support info. Does not auto-delete duplicates. This should now be based on support info.
@ -638,12 +770,12 @@ def coin_info():
return all_coins return all_coins
def fido_info(): def fido_info() -> FidoApps:
"""Returns info about known FIDO/U2F apps.""" """Returns info about known FIDO/U2F apps."""
return _load_fido_apps() return _load_fido_apps()
def search(coins, keyword): def search(coins: CoinsInfo | Coins, keyword: str) -> Iterator[Any]:
kwl = keyword.lower() kwl = keyword.lower()
if isinstance(coins, CoinsInfo): if isinstance(coins, CoinsInfo):
coins = coins.as_list() coins = coins.as_list()

@ -1,4 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import fnmatch import fnmatch
import glob import glob
import json import json
@ -8,10 +10,12 @@ import re
import sys import sys
from collections import defaultdict from collections import defaultdict
from hashlib import sha256 from hashlib import sha256
from typing import Any, Callable, Iterator, TextIO, cast
import click import click
import coin_info import coin_info
from coin_info import Coin, CoinBuckets, Coins, CoinsInfo, FidoApps, SupportInfo
try: try:
import termcolor import termcolor
@ -44,7 +48,9 @@ except ImportError:
USE_COLORS = False USE_COLORS = False
def crayon(color, string, bold=False, dim=False): def crayon(
color: str | None, string: str, bold: bool = False, dim: bool = False
) -> str:
if not termcolor or not USE_COLORS: if not termcolor or not USE_COLORS:
return string return string
else: else:
@ -57,7 +63,7 @@ def crayon(color, string, bold=False, dim=False):
return termcolor.colored(string, color, attrs=attrs) return termcolor.colored(string, color, attrs=attrs)
def print_log(level, *args, **kwargs): def print_log(level: int, *args: Any, **kwargs: Any) -> None:
prefix = logging.getLevelName(level) prefix = logging.getLevelName(level)
if level == logging.DEBUG: if level == logging.DEBUG:
prefix = crayon("blue", prefix, bold=False) prefix = crayon("blue", prefix, bold=False)
@ -73,11 +79,11 @@ def print_log(level, *args, **kwargs):
# ======= Mako management ====== # ======= Mako management ======
def c_str_filter(b): def c_str_filter(b: Any) -> str:
if b is None: if b is None:
return "NULL" return "NULL"
def hexescape(c): def hexescape(c: bytes) -> str:
return rf"\x{c:02x}" return rf"\x{c:02x}"
if isinstance(b, bytes): if isinstance(b, bytes):
@ -86,7 +92,7 @@ def c_str_filter(b):
return json.dumps(b) return json.dumps(b)
def black_repr_filter(val): def black_repr_filter(val: Any) -> str:
if isinstance(val, str): if isinstance(val, str):
if '"' in val: if '"' in val:
return repr(val) return repr(val)
@ -98,12 +104,14 @@ def black_repr_filter(val):
return repr(val) return repr(val)
def ascii_filter(s): def ascii_filter(s: str) -> str:
return re.sub("[^ -\x7e]", "_", s) return re.sub("[^ -\x7e]", "_", s)
def make_support_filter(support_info): def make_support_filter(
def supported_on(device, coins): support_info: SupportInfo,
) -> Callable[[str, Coins], Iterator[Coin]]:
def supported_on(device: str, coins: Coins) -> Iterator[Coin]:
return (c for c in coins if support_info[c.key].get(device)) return (c for c in coins if support_info[c.key].get(device))
return supported_on return supported_on
@ -116,7 +124,9 @@ MAKO_FILTERS = {
} }
def render_file(src, dst, coins, support_info): def render_file(
src: str, dst: TextIO, coins: CoinsInfo, support_info: SupportInfo
) -> None:
"""Renders `src` template into `dst`. """Renders `src` template into `dst`.
`src` is a filename, `dst` is an open file object. `src` is a filename, `dst` is an open file object.
@ -134,14 +144,14 @@ def render_file(src, dst, coins, support_info):
# ====== validation functions ====== # ====== validation functions ======
def mark_unsupported(support_info, coins): def mark_unsupported(support_info: SupportInfo, coins: Coins) -> None:
for coin in coins: for coin in coins:
key = coin["key"] key = coin["key"]
# checking for explicit False because None means unknown # checking for explicit False because None means unknown
coin["unsupported"] = all(v is False for v in support_info[key].values()) coin["unsupported"] = all(v is False for v in support_info[key].values())
def highlight_key(coin, color): def highlight_key(coin: Coin, color: str) -> str:
"""Return a colorful string where the SYMBOL part is bold.""" """Return a colorful string where the SYMBOL part is bold."""
keylist = coin["key"].split(":") keylist = coin["key"].split(":")
if keylist[-1].isdigit(): if keylist[-1].isdigit():
@ -153,9 +163,9 @@ def highlight_key(coin, color):
return f"{key} {name}" return f"{key} {name}"
def find_collisions(coins, field): def find_collisions(coins: Coins, field: str) -> CoinBuckets:
"""Detects collisions in a given field. Returns buckets of colliding coins.""" """Detects collisions in a given field. Returns buckets of colliding coins."""
collisions = defaultdict(list) collisions: CoinBuckets = defaultdict(list)
for coin in coins: for coin in coins:
values = coin[field] values = coin[field]
if not isinstance(values, list): if not isinstance(values, list):
@ -165,7 +175,7 @@ def find_collisions(coins, field):
return {k: v for k, v in collisions.items() if len(v) > 1} return {k: v for k, v in collisions.items() if len(v) > 1}
def check_eth(coins): def check_eth(coins: Coins) -> bool:
check_passed = True check_passed = True
chains = find_collisions(coins, "chain") chains = find_collisions(coins, "chain")
for key, bucket in chains.items(): for key, bucket in chains.items():
@ -176,7 +186,7 @@ def check_eth(coins):
return check_passed return check_passed
def check_btc(coins): def check_btc(coins: Coins) -> bool:
check_passed = True check_passed = True
# validate individual coin data # validate individual coin data
@ -187,9 +197,9 @@ def check_btc(coins):
print_log(logging.ERROR, "invalid definition for", coin["name"]) print_log(logging.ERROR, "invalid definition for", coin["name"])
print("\n".join(errors)) print("\n".join(errors))
def collision_str(bucket): def collision_str(bucket: Coins) -> str:
"""Generate a colorful string out of a bucket of colliding coins.""" """Generate a colorful string out of a bucket of colliding coins."""
coin_strings = [] coin_strings: list[str] = []
for coin in bucket: for coin in bucket:
name = coin["name"] name = coin["name"]
prefix = "" prefix = ""
@ -206,7 +216,12 @@ def check_btc(coins):
coin_strings.append(prefix + hl) coin_strings.append(prefix + hl)
return ", ".join(coin_strings) return ", ".join(coin_strings)
def print_collision_buckets(buckets, prefix, maxlevel=logging.ERROR, strict=False): def print_collision_buckets(
buckets: CoinBuckets,
prefix: str,
maxlevel: int = logging.ERROR,
strict: bool = False,
) -> bool:
"""Intelligently print collision buckets. """Intelligently print collision buckets.
For each bucket, if there are any collision with a mainnet, print it. For each bucket, if there are any collision with a mainnet, print it.
@ -268,7 +283,7 @@ def check_btc(coins):
return check_passed return check_passed
def check_dups(buckets, print_at_level=logging.WARNING): def check_dups(buckets: CoinBuckets, print_at_level: int = logging.WARNING) -> bool:
"""Analyze and pretty-print results of `coin_info.mark_duplicate_shortcuts`. """Analyze and pretty-print results of `coin_info.mark_duplicate_shortcuts`.
`print_at_level` can be one of logging levels. `print_at_level` can be one of logging levels.
@ -279,7 +294,7 @@ def check_dups(buckets, print_at_level=logging.WARNING):
If the collision includes more than one non-token, it's ERROR and printed always. If the collision includes more than one non-token, it's ERROR and printed always.
""" """
def coin_str(coin): def coin_str(coin: Coin) -> str:
"""Colorize coins. Tokens are cyan, nontokens are red. Coins that are NOT """Colorize coins. Tokens are cyan, nontokens are red. Coins that are NOT
marked duplicate get a green asterisk. marked duplicate get a green asterisk.
""" """
@ -347,7 +362,7 @@ def check_dups(buckets, print_at_level=logging.WARNING):
return check_passed return check_passed
def check_backends(coins): def check_backends(coins: Coins) -> bool:
check_passed = True check_passed = True
for coin in coins: for coin in coins:
genesis_block = coin.get("hash_genesis_block") genesis_block = coin.get("hash_genesis_block")
@ -357,6 +372,7 @@ def check_backends(coins):
for backend in backends: for backend in backends:
print("checking", backend, "... ", end="", flush=True) print("checking", backend, "... ", end="", flush=True)
try: try:
assert requests is not None
j = requests.get(backend + "/api/block-index/0").json() j = requests.get(backend + "/api/block-index/0").json()
if j["blockHash"] != genesis_block: if j["blockHash"] != genesis_block:
raise RuntimeError("genesis block mismatch") raise RuntimeError("genesis block mismatch")
@ -368,7 +384,7 @@ def check_backends(coins):
return check_passed return check_passed
def check_icons(coins): def check_icons(coins: Coins) -> bool:
check_passed = True check_passed = True
for coin in coins: for coin in coins:
key = coin["key"] key = coin["key"]
@ -394,8 +410,8 @@ def check_icons(coins):
IGNORE_NONUNIFORM_KEYS = frozenset(("unsupported", "duplicate")) IGNORE_NONUNIFORM_KEYS = frozenset(("unsupported", "duplicate"))
def check_key_uniformity(coins): def check_key_uniformity(coins: Coins) -> bool:
keysets = defaultdict(list) keysets: dict[frozenset[str], Coins] = defaultdict(list)
for coin in coins: for coin in coins:
keyset = frozenset(coin.keys()) | IGNORE_NONUNIFORM_KEYS keyset = frozenset(coin.keys()) | IGNORE_NONUNIFORM_KEYS
keysets[keyset].append(coin) keysets[keyset].append(coin)
@ -426,7 +442,7 @@ def check_key_uniformity(coins):
return False return False
def check_segwit(coins): def check_segwit(coins: Coins) -> bool:
for coin in coins: for coin in coins:
segwit = coin["segwit"] segwit = coin["segwit"]
segwit_fields = [ segwit_fields = [
@ -471,7 +487,7 @@ FIDO_KNOWN_KEYS = frozenset(
) )
def check_fido(apps): def check_fido(apps: FidoApps) -> bool:
check_passed = True check_passed = True
u2fs = find_collisions((u for a in apps if "u2f" in a for u in a["u2f"]), "app_id") u2fs = find_collisions((u for a in apps if "u2f" in a for u in a["u2f"]), "app_id")
@ -488,7 +504,7 @@ def check_fido(apps):
print_log(logging.ERROR, webauthn_str, bucket_str) print_log(logging.ERROR, webauthn_str, bucket_str)
check_passed = False check_passed = False
domain_hashes = {} domain_hashes: dict[bytes, str] = {}
for app in apps: for app in apps:
if "webauthn" in app: if "webauthn" in app:
for domain in app["webauthn"]: for domain in app["webauthn"]:
@ -574,7 +590,7 @@ def check_fido(apps):
default=sys.stdout.isatty(), default=sys.stdout.isatty(),
help="Force colored output on/off", help="Force colored output on/off",
) )
def cli(colors): def cli(colors: bool) -> None:
global USE_COLORS global USE_COLORS
USE_COLORS = colors USE_COLORS = colors
@ -585,7 +601,7 @@ def cli(colors):
@click.option("--icons/--no-icons", default=True, help="Check icon files") @click.option("--icons/--no-icons", default=True, help="Check icon files")
@click.option("-d", "--show-duplicates", type=click.Choice(("all", "nontoken", "errors")), default="errors", help="How much information about duplicate shortcuts should be shown.") @click.option("-d", "--show-duplicates", type=click.Choice(("all", "nontoken", "errors")), default="errors", help="How much information about duplicate shortcuts should be shown.")
# fmt: on # fmt: on
def check(backend, icons, show_duplicates): def check(backend: bool, icons: bool, show_duplicates: str) -> None:
"""Validate coin definitions. """Validate coin definitions.
Checks that every btc-like coin is properly filled out, reports duplicate symbols, Checks that every btc-like coin is properly filled out, reports duplicate symbols,
@ -703,19 +719,19 @@ def check(backend, icons, show_duplicates):
@click.option("-d", "--device", metavar="NAME", help="Only include coins supported on a given device") @click.option("-d", "--device", metavar="NAME", help="Only include coins supported on a given device")
# fmt: on # fmt: on
def dump( def dump(
outfile, outfile: TextIO,
support, support: bool,
pretty, pretty: bool,
flat_list, flat_list: bool,
include, include: tuple[str, ...],
exclude, exclude: tuple[str, ...],
include_type, include_type: tuple[str, ...],
exclude_type, exclude_type: tuple[str, ...],
filter, filter: tuple[str, ...],
filter_exclude, filter_exclude: tuple[str, ...],
exclude_tokens, exclude_tokens: bool,
device, device: str,
): ) -> None:
"""Dump coin data in JSON format. """Dump coin data in JSON format.
This file is structured the same as the internal data. That is, top-level object This file is structured the same as the internal data. That is, top-level object
@ -744,7 +760,7 @@ def dump(
so '-f name=bit*' finds all coins whose names start with "bit" or "Bit". so '-f name=bit*' finds all coins whose names start with "bit" or "Bit".
""" """
if exclude_tokens: if exclude_tokens:
exclude_type = ("erc20",) exclude_type = ["erc20"]
if include and exclude: if include and exclude:
raise click.ClickException( raise click.ClickException(
@ -776,7 +792,7 @@ def dump(
# always exclude 'address_bytes', not encodable in JSON # always exclude 'address_bytes', not encodable in JSON
exclude += ("address_bytes",) exclude += ("address_bytes",)
def should_include_coin(coin): def should_include_coin(coin: Coin) -> bool:
for field, filter in include_filters: for field, filter in include_filters:
filter = filter.lower() filter = filter.lower()
if field not in coin: if field not in coin:
@ -795,11 +811,11 @@ def dump(
return False return False
return True return True
def modify_coin(coin): def modify_coin(coin: Coin) -> Coin:
if include: if include:
return {k: v for k, v in coin.items() if k in include} return cast(Coin, {k: v for k, v in coin.items() if k in include})
else: else:
return {k: v for k, v in coin.items() if k not in exclude} return cast(Coin, {k: v for k, v in coin.items() if k not in exclude})
for key, coinlist in coins_dict.items(): for key, coinlist in coins_dict.items():
coins_dict[key] = [modify_coin(c) for c in coinlist if should_include_coin(c)] coins_dict[key] = [modify_coin(c) for c in coinlist if should_include_coin(c)]
@ -822,7 +838,9 @@ def dump(
@click.option("-v", "--verbose", is_flag=True, help="Print rendered file names") @click.option("-v", "--verbose", is_flag=True, help="Print rendered file names")
@click.option("-b", "--bitcoin-only", is_flag=True, help="Accept only Bitcoin coins") @click.option("-b", "--bitcoin-only", is_flag=True, help="Accept only Bitcoin coins")
# fmt: on # fmt: on
def render(paths, outfile, verbose, bitcoin_only): def render(
paths: tuple[str, ...], outfile: TextIO, verbose: bool, bitcoin_only: bool
) -> None:
"""Generate source code from Mako templates. """Generate source code from Mako templates.
For every "foo.bar.mako" filename passed, runs the template and For every "foo.bar.mako" filename passed, runs the template and
@ -857,9 +875,9 @@ def render(paths, outfile, verbose, bitcoin_only):
for key, value in support_info.items(): for key, value in support_info.items():
support_info[key] = Munch(value) support_info[key] = Munch(value)
def do_render(src, dst): def do_render(src: str, dst: TextIO) -> None:
if verbose: if verbose:
click.echo(f"Rendering {src} => {dst}") click.echo(f"Rendering {src} => {dst.name}")
render_file(src, dst, defs, support_info) render_file(src, dst, defs, support_info)
# single in-out case # single in-out case
@ -869,9 +887,9 @@ def render(paths, outfile, verbose, bitcoin_only):
# find files in directories # find files in directories
if not paths: if not paths:
paths = ["."] paths = (".",)
files = [] files: list[str] = []
for path in paths: for path in paths:
if not os.path.exists(path): if not os.path.exists(path):
click.echo(f"Path {path} does not exist") click.echo(f"Path {path} does not exist")

Loading…
Cancel
Save