mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-21 23:18:13 +00:00
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.
This commit is contained in:
parent
5671bd037b
commit
519f79f9eb
@ -1,10 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from collections import OrderedDict, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Iterable, Iterator, Literal, TypedDict, cast
|
||||
|
||||
try:
|
||||
import requests
|
||||
@ -21,7 +24,125 @@ else:
|
||||
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."""
|
||||
if len(path) == 1 and isinstance(path[0], Path):
|
||||
file = path[0]
|
||||
@ -34,7 +155,7 @@ def load_json(*path):
|
||||
# ====== CoinsInfo ======
|
||||
|
||||
|
||||
class CoinsInfo(dict):
|
||||
class CoinsInfo(dict[str, Coins]):
|
||||
"""Collection of information about all known kinds of coins.
|
||||
|
||||
It contains the following lists:
|
||||
@ -47,13 +168,13 @@ class CoinsInfo(dict):
|
||||
Accessible as a dict or by attribute: `info["misc"] == info.misc`
|
||||
"""
|
||||
|
||||
def as_list(self):
|
||||
def as_list(self) -> Coins:
|
||||
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()}
|
||||
|
||||
def __getattr__(self, attr):
|
||||
def __getattr__(self, attr: str) -> Coins:
|
||||
if attr in self:
|
||||
return self[attr]
|
||||
else:
|
||||
@ -63,7 +184,14 @@ class CoinsInfo(dict):
|
||||
# ====== 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
|
||||
if val is None:
|
||||
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 types is not str:
|
||||
raise TypeError("Wrong type for regex check")
|
||||
assert isinstance(val, str)
|
||||
if not re.search(regex, val):
|
||||
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}")
|
||||
|
||||
|
||||
def check_key(key, types, optional=False, **kwargs):
|
||||
def do_check(coin):
|
||||
def check_key(
|
||||
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 optional:
|
||||
return
|
||||
@ -152,15 +283,15 @@ BTC_CHECKS = [
|
||||
]
|
||||
|
||||
|
||||
def validate_btc(coin):
|
||||
errors = []
|
||||
def validate_btc(coin: Coin) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for check in BTC_CHECKS:
|
||||
try:
|
||||
check(coin)
|
||||
except Exception as e:
|
||||
errors.append(str(e))
|
||||
|
||||
magics = [
|
||||
magics: list[int] = [
|
||||
coin[k]
|
||||
for k in (
|
||||
"xprv_magic",
|
||||
@ -208,11 +339,11 @@ def validate_btc(coin):
|
||||
# ======= Coin json loaders =======
|
||||
|
||||
|
||||
def _load_btc_coins():
|
||||
def _load_btc_coins() -> Coins:
|
||||
"""Load btc-like coins from `bitcoin/*.json`"""
|
||||
coins = []
|
||||
coins: Coins = []
|
||||
for file in DEFS_DIR.glob("bitcoin/*.json"):
|
||||
coin = load_json(file)
|
||||
coin: Coin = load_json(file)
|
||||
coin.update(
|
||||
name=coin["coin_label"],
|
||||
shortcut=coin["coin_shortcut"],
|
||||
@ -224,10 +355,10 @@ def _load_btc_coins():
|
||||
return coins
|
||||
|
||||
|
||||
def _load_ethereum_networks():
|
||||
def _load_ethereum_networks() -> Coins:
|
||||
"""Load ethereum networks from `ethereum/networks.json`"""
|
||||
chains_path = DEFS_DIR / "ethereum" / "chains" / "_data" / "chains"
|
||||
networks = []
|
||||
networks: Coins = []
|
||||
for chain in sorted(
|
||||
chains_path.glob("eip155-*.json"),
|
||||
key=lambda x: int(x.stem.replace("eip155-", "")),
|
||||
@ -261,21 +392,21 @@ def _load_ethereum_networks():
|
||||
url=chain_data["infoURL"],
|
||||
key=f"eth:{shortcut}",
|
||||
)
|
||||
networks.append(network)
|
||||
networks.append(cast(Coin, network))
|
||||
|
||||
return networks
|
||||
|
||||
|
||||
def _load_erc20_tokens():
|
||||
def _load_erc20_tokens() -> Coins:
|
||||
"""Load ERC20 tokens from `ethereum/tokens` submodule."""
|
||||
networks = _load_ethereum_networks()
|
||||
tokens = []
|
||||
tokens: Coins = []
|
||||
for network in networks:
|
||||
chain = network["chain"]
|
||||
|
||||
chain_path = DEFS_DIR / "ethereum" / "tokens" / "tokens" / chain
|
||||
for file in sorted(chain_path.glob("*.json")):
|
||||
token = load_json(file)
|
||||
token: Coin = load_json(file)
|
||||
token.update(
|
||||
chain=chain,
|
||||
chain_id=network["chain_id"],
|
||||
@ -288,26 +419,26 @@ def _load_erc20_tokens():
|
||||
return tokens
|
||||
|
||||
|
||||
def _load_nem_mosaics():
|
||||
def _load_nem_mosaics() -> Coins:
|
||||
"""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:
|
||||
shortcut = mosaic["ticker"].strip()
|
||||
mosaic.update(shortcut=shortcut, key=f"nem:{shortcut}")
|
||||
return mosaics
|
||||
|
||||
|
||||
def _load_misc():
|
||||
def _load_misc() -> Coins:
|
||||
"""Loads miscellaneous networks from `misc/misc.json`"""
|
||||
others = load_json("misc/misc.json")
|
||||
others: Coins = load_json("misc/misc.json")
|
||||
for other in others:
|
||||
other.update(key=f"misc:{other['shortcut']}")
|
||||
return others
|
||||
|
||||
|
||||
def _load_fido_apps():
|
||||
def _load_fido_apps() -> FidoApps:
|
||||
"""Load FIDO apps from `fido/*.json`"""
|
||||
apps = []
|
||||
apps: FidoApps = []
|
||||
for file in sorted(DEFS_DIR.glob("fido/*.json")):
|
||||
app_name = file.stem.lower()
|
||||
app = load_json(file)
|
||||
@ -335,28 +466,28 @@ MISSING_SUPPORT_MEANS_NO = ("connect", "suite")
|
||||
VERSIONED_SUPPORT_INFO = ("trezor1", "trezor2")
|
||||
|
||||
|
||||
def get_support_data():
|
||||
def get_support_data() -> SupportData:
|
||||
"""Get raw support data from `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"""
|
||||
if not requests:
|
||||
raise RuntimeError("requests library is required for getting release info")
|
||||
|
||||
latest = {}
|
||||
latest: dict[str, Any] = {}
|
||||
for v in ("1", "2"):
|
||||
releases = requests.get(RELEASES_URL.format(v)).json()
|
||||
latest["trezor" + v] = max(tuple(r["version"]) for r in releases)
|
||||
return latest
|
||||
|
||||
|
||||
def is_token(coin):
|
||||
def is_token(coin: Coin) -> bool:
|
||||
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.
|
||||
|
||||
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)
|
||||
* if the coin doesn't have an entry, its support status is `None`
|
||||
"""
|
||||
support_info = {}
|
||||
support_info_item = {}
|
||||
key = coin["key"]
|
||||
for device, values in support_data.items():
|
||||
assert isinstance(values, dict)
|
||||
if key in values["unsupported"]:
|
||||
support_value = False
|
||||
support_value: Any = False
|
||||
elif key in values["supported"]:
|
||||
support_value = values["supported"][key]
|
||||
elif device in MISSING_SUPPORT_MEANS_NO:
|
||||
support_value = False
|
||||
else:
|
||||
support_value = None
|
||||
support_info[device] = support_value
|
||||
return support_info
|
||||
support_info_item[device] = support_value
|
||||
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.
|
||||
|
||||
Takes a collection of coins and generates a support-info entry for each.
|
||||
@ -401,7 +533,7 @@ def support_info(coins):
|
||||
coins = coins.values()
|
||||
|
||||
support_data = get_support_data()
|
||||
support = {}
|
||||
support: SupportInfo = {}
|
||||
for coin in coins:
|
||||
support[coin["key"]] = support_info_single(support_data, coin)
|
||||
|
||||
@ -411,19 +543,19 @@ def support_info(coins):
|
||||
# ====== 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"""
|
||||
for coin in coins:
|
||||
if not all(coin.get(k) for k in ("name", "shortcut", "key")):
|
||||
raise ValueError(coin)
|
||||
|
||||
|
||||
def symbol_from_shortcut(shortcut):
|
||||
def symbol_from_shortcut(shortcut: str) -> tuple[str, str]:
|
||||
symsplit = shortcut.split(" ", maxsplit=1)
|
||||
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.
|
||||
|
||||
"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
|
||||
it's explicitly marked as `false` in `duplicity_overrides.json`.
|
||||
"""
|
||||
dup_symbols = defaultdict(list)
|
||||
dup_symbols: CoinBuckets = defaultdict(list)
|
||||
|
||||
for coin in coins:
|
||||
symbol, _ = symbol_from_shortcut(coin["shortcut"].lower())
|
||||
@ -451,9 +583,9 @@ def mark_duplicate_shortcuts(coins):
|
||||
return dup_symbols
|
||||
|
||||
|
||||
def apply_duplicity_overrides(coins):
|
||||
def apply_duplicity_overrides(coins: Coins) -> Coins:
|
||||
overrides = load_json("duplicity_overrides.json")
|
||||
override_bucket = []
|
||||
override_bucket: Coins = []
|
||||
for coin in coins:
|
||||
override_value = overrides.get(coin["key"])
|
||||
if override_value is True:
|
||||
@ -464,7 +596,7 @@ def apply_duplicity_overrides(coins):
|
||||
return override_bucket
|
||||
|
||||
|
||||
def deduplicate_erc20(buckets, networks):
|
||||
def deduplicate_erc20(buckets: CoinBuckets, networks: Coins) -> None:
|
||||
"""Apply further processing to ERC20 duplicate buckets.
|
||||
|
||||
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"]}
|
||||
|
||||
def clear_bucket(bucket):
|
||||
def clear_bucket(bucket: Coins) -> None:
|
||||
# allow all coins, except those that are explicitly marked through overrides
|
||||
for coin in bucket:
|
||||
coin["duplicate"] = False
|
||||
@ -531,8 +663,8 @@ def deduplicate_erc20(buckets, networks):
|
||||
clear_bucket(bucket)
|
||||
|
||||
|
||||
def deduplicate_keys(all_coins):
|
||||
dups = defaultdict(list)
|
||||
def deduplicate_keys(all_coins: Coins) -> None:
|
||||
dups: CoinBuckets = defaultdict(list)
|
||||
for coin in all_coins:
|
||||
dups[coin["key"]].append(coin)
|
||||
|
||||
@ -549,7 +681,7 @@ def deduplicate_keys(all_coins):
|
||||
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")
|
||||
for coins in all_coins.values():
|
||||
for coin in coins:
|
||||
@ -561,14 +693,14 @@ def fill_blockchain_links(all_coins):
|
||||
coin["blockbook"] = []
|
||||
|
||||
|
||||
def _btc_sort_key(coin):
|
||||
def _btc_sort_key(coin: Coin) -> str:
|
||||
if coin["name"] in ("Bitcoin", "Testnet", "Regtest"):
|
||||
return "000000" + coin["name"]
|
||||
else:
|
||||
return coin["name"]
|
||||
|
||||
|
||||
def collect_coin_info():
|
||||
def collect_coin_info() -> CoinsInfo:
|
||||
"""Returns all definition as dict organized by coin type.
|
||||
`coins` for btc-like coins,
|
||||
`eth` for ethereum networks,
|
||||
@ -592,7 +724,7 @@ def collect_coin_info():
|
||||
return all_coins
|
||||
|
||||
|
||||
def sort_coin_infos(all_coins):
|
||||
def sort_coin_infos(all_coins: CoinsInfo) -> None:
|
||||
for k, coins in all_coins.items():
|
||||
if k == "bitcoin":
|
||||
coins.sort(key=_btc_sort_key)
|
||||
@ -606,7 +738,7 @@ def sort_coin_infos(all_coins):
|
||||
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.
|
||||
|
||||
Returns the CoinsInfo object and duplicate buckets.
|
||||
@ -626,7 +758,7 @@ def coin_info_with_duplicates():
|
||||
return all_coins, buckets
|
||||
|
||||
|
||||
def coin_info():
|
||||
def coin_info() -> CoinsInfo:
|
||||
"""Collects coin info, fills out support info and returns the result.
|
||||
|
||||
Does not auto-delete duplicates. This should now be based on support info.
|
||||
@ -638,12 +770,12 @@ def coin_info():
|
||||
return all_coins
|
||||
|
||||
|
||||
def fido_info():
|
||||
def fido_info() -> FidoApps:
|
||||
"""Returns info about known FIDO/U2F apps."""
|
||||
return _load_fido_apps()
|
||||
|
||||
|
||||
def search(coins, keyword):
|
||||
def search(coins: CoinsInfo | Coins, keyword: str) -> Iterator[Any]:
|
||||
kwl = keyword.lower()
|
||||
if isinstance(coins, CoinsInfo):
|
||||
coins = coins.as_list()
|
||||
|
@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import glob
|
||||
import json
|
||||
@ -8,10 +10,12 @@ import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from hashlib import sha256
|
||||
from typing import Any, Callable, Iterator, TextIO, cast
|
||||
|
||||
import click
|
||||
|
||||
import coin_info
|
||||
from coin_info import Coin, CoinBuckets, Coins, CoinsInfo, FidoApps, SupportInfo
|
||||
|
||||
try:
|
||||
import termcolor
|
||||
@ -44,7 +48,9 @@ except ImportError:
|
||||
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:
|
||||
return string
|
||||
else:
|
||||
@ -57,7 +63,7 @@ def crayon(color, string, bold=False, dim=False):
|
||||
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)
|
||||
if level == logging.DEBUG:
|
||||
prefix = crayon("blue", prefix, bold=False)
|
||||
@ -73,11 +79,11 @@ def print_log(level, *args, **kwargs):
|
||||
# ======= Mako management ======
|
||||
|
||||
|
||||
def c_str_filter(b):
|
||||
def c_str_filter(b: Any) -> str:
|
||||
if b is None:
|
||||
return "NULL"
|
||||
|
||||
def hexescape(c):
|
||||
def hexescape(c: bytes) -> str:
|
||||
return rf"\x{c:02x}"
|
||||
|
||||
if isinstance(b, bytes):
|
||||
@ -86,7 +92,7 @@ def c_str_filter(b):
|
||||
return json.dumps(b)
|
||||
|
||||
|
||||
def black_repr_filter(val):
|
||||
def black_repr_filter(val: Any) -> str:
|
||||
if isinstance(val, str):
|
||||
if '"' in val:
|
||||
return repr(val)
|
||||
@ -98,12 +104,14 @@ def black_repr_filter(val):
|
||||
return repr(val)
|
||||
|
||||
|
||||
def ascii_filter(s):
|
||||
def ascii_filter(s: str) -> str:
|
||||
return re.sub("[^ -\x7e]", "_", s)
|
||||
|
||||
|
||||
def make_support_filter(support_info):
|
||||
def supported_on(device, coins):
|
||||
def make_support_filter(
|
||||
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 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`.
|
||||
|
||||
`src` is a filename, `dst` is an open file object.
|
||||
@ -134,14 +144,14 @@ def render_file(src, dst, coins, support_info):
|
||||
# ====== validation functions ======
|
||||
|
||||
|
||||
def mark_unsupported(support_info, coins):
|
||||
def mark_unsupported(support_info: SupportInfo, coins: Coins) -> None:
|
||||
for coin in coins:
|
||||
key = coin["key"]
|
||||
# checking for explicit False because None means unknown
|
||||
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."""
|
||||
keylist = coin["key"].split(":")
|
||||
if keylist[-1].isdigit():
|
||||
@ -153,9 +163,9 @@ def highlight_key(coin, color):
|
||||
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."""
|
||||
collisions = defaultdict(list)
|
||||
collisions: CoinBuckets = defaultdict(list)
|
||||
for coin in coins:
|
||||
values = coin[field]
|
||||
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}
|
||||
|
||||
|
||||
def check_eth(coins):
|
||||
def check_eth(coins: Coins) -> bool:
|
||||
check_passed = True
|
||||
chains = find_collisions(coins, "chain")
|
||||
for key, bucket in chains.items():
|
||||
@ -176,7 +186,7 @@ def check_eth(coins):
|
||||
return check_passed
|
||||
|
||||
|
||||
def check_btc(coins):
|
||||
def check_btc(coins: Coins) -> bool:
|
||||
check_passed = True
|
||||
|
||||
# validate individual coin data
|
||||
@ -187,9 +197,9 @@ def check_btc(coins):
|
||||
print_log(logging.ERROR, "invalid definition for", coin["name"])
|
||||
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."""
|
||||
coin_strings = []
|
||||
coin_strings: list[str] = []
|
||||
for coin in bucket:
|
||||
name = coin["name"]
|
||||
prefix = ""
|
||||
@ -206,7 +216,12 @@ def check_btc(coins):
|
||||
coin_strings.append(prefix + hl)
|
||||
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.
|
||||
|
||||
For each bucket, if there are any collision with a mainnet, print it.
|
||||
@ -268,7 +283,7 @@ def check_btc(coins):
|
||||
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`.
|
||||
|
||||
`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.
|
||||
"""
|
||||
|
||||
def coin_str(coin):
|
||||
def coin_str(coin: Coin) -> str:
|
||||
"""Colorize coins. Tokens are cyan, nontokens are red. Coins that are NOT
|
||||
marked duplicate get a green asterisk.
|
||||
"""
|
||||
@ -347,7 +362,7 @@ def check_dups(buckets, print_at_level=logging.WARNING):
|
||||
return check_passed
|
||||
|
||||
|
||||
def check_backends(coins):
|
||||
def check_backends(coins: Coins) -> bool:
|
||||
check_passed = True
|
||||
for coin in coins:
|
||||
genesis_block = coin.get("hash_genesis_block")
|
||||
@ -357,6 +372,7 @@ def check_backends(coins):
|
||||
for backend in backends:
|
||||
print("checking", backend, "... ", end="", flush=True)
|
||||
try:
|
||||
assert requests is not None
|
||||
j = requests.get(backend + "/api/block-index/0").json()
|
||||
if j["blockHash"] != genesis_block:
|
||||
raise RuntimeError("genesis block mismatch")
|
||||
@ -368,7 +384,7 @@ def check_backends(coins):
|
||||
return check_passed
|
||||
|
||||
|
||||
def check_icons(coins):
|
||||
def check_icons(coins: Coins) -> bool:
|
||||
check_passed = True
|
||||
for coin in coins:
|
||||
key = coin["key"]
|
||||
@ -394,8 +410,8 @@ def check_icons(coins):
|
||||
IGNORE_NONUNIFORM_KEYS = frozenset(("unsupported", "duplicate"))
|
||||
|
||||
|
||||
def check_key_uniformity(coins):
|
||||
keysets = defaultdict(list)
|
||||
def check_key_uniformity(coins: Coins) -> bool:
|
||||
keysets: dict[frozenset[str], Coins] = defaultdict(list)
|
||||
for coin in coins:
|
||||
keyset = frozenset(coin.keys()) | IGNORE_NONUNIFORM_KEYS
|
||||
keysets[keyset].append(coin)
|
||||
@ -426,7 +442,7 @@ def check_key_uniformity(coins):
|
||||
return False
|
||||
|
||||
|
||||
def check_segwit(coins):
|
||||
def check_segwit(coins: Coins) -> bool:
|
||||
for coin in coins:
|
||||
segwit = coin["segwit"]
|
||||
segwit_fields = [
|
||||
@ -471,7 +487,7 @@ FIDO_KNOWN_KEYS = frozenset(
|
||||
)
|
||||
|
||||
|
||||
def check_fido(apps):
|
||||
def check_fido(apps: FidoApps) -> bool:
|
||||
check_passed = True
|
||||
|
||||
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)
|
||||
check_passed = False
|
||||
|
||||
domain_hashes = {}
|
||||
domain_hashes: dict[bytes, str] = {}
|
||||
for app in apps:
|
||||
if "webauthn" in app:
|
||||
for domain in app["webauthn"]:
|
||||
@ -574,7 +590,7 @@ def check_fido(apps):
|
||||
default=sys.stdout.isatty(),
|
||||
help="Force colored output on/off",
|
||||
)
|
||||
def cli(colors):
|
||||
def cli(colors: bool) -> None:
|
||||
global USE_COLORS
|
||||
USE_COLORS = colors
|
||||
|
||||
@ -585,7 +601,7 @@ def cli(colors):
|
||||
@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.")
|
||||
# fmt: on
|
||||
def check(backend, icons, show_duplicates):
|
||||
def check(backend: bool, icons: bool, show_duplicates: str) -> None:
|
||||
"""Validate coin definitions.
|
||||
|
||||
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")
|
||||
# fmt: on
|
||||
def dump(
|
||||
outfile,
|
||||
support,
|
||||
pretty,
|
||||
flat_list,
|
||||
include,
|
||||
exclude,
|
||||
include_type,
|
||||
exclude_type,
|
||||
filter,
|
||||
filter_exclude,
|
||||
exclude_tokens,
|
||||
device,
|
||||
):
|
||||
outfile: TextIO,
|
||||
support: bool,
|
||||
pretty: bool,
|
||||
flat_list: bool,
|
||||
include: tuple[str, ...],
|
||||
exclude: tuple[str, ...],
|
||||
include_type: tuple[str, ...],
|
||||
exclude_type: tuple[str, ...],
|
||||
filter: tuple[str, ...],
|
||||
filter_exclude: tuple[str, ...],
|
||||
exclude_tokens: bool,
|
||||
device: str,
|
||||
) -> None:
|
||||
"""Dump coin data in JSON format.
|
||||
|
||||
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".
|
||||
"""
|
||||
if exclude_tokens:
|
||||
exclude_type = ("erc20",)
|
||||
exclude_type = ["erc20"]
|
||||
|
||||
if include and exclude:
|
||||
raise click.ClickException(
|
||||
@ -776,7 +792,7 @@ def dump(
|
||||
# always exclude 'address_bytes', not encodable in JSON
|
||||
exclude += ("address_bytes",)
|
||||
|
||||
def should_include_coin(coin):
|
||||
def should_include_coin(coin: Coin) -> bool:
|
||||
for field, filter in include_filters:
|
||||
filter = filter.lower()
|
||||
if field not in coin:
|
||||
@ -795,11 +811,11 @@ def dump(
|
||||
return False
|
||||
return True
|
||||
|
||||
def modify_coin(coin):
|
||||
def modify_coin(coin: Coin) -> Coin:
|
||||
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:
|
||||
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():
|
||||
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("-b", "--bitcoin-only", is_flag=True, help="Accept only Bitcoin coins")
|
||||
# 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.
|
||||
|
||||
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():
|
||||
support_info[key] = Munch(value)
|
||||
|
||||
def do_render(src, dst):
|
||||
def do_render(src: str, dst: TextIO) -> None:
|
||||
if verbose:
|
||||
click.echo(f"Rendering {src} => {dst}")
|
||||
click.echo(f"Rendering {src} => {dst.name}")
|
||||
render_file(src, dst, defs, support_info)
|
||||
|
||||
# single in-out case
|
||||
@ -869,9 +887,9 @@ def render(paths, outfile, verbose, bitcoin_only):
|
||||
|
||||
# find files in directories
|
||||
if not paths:
|
||||
paths = ["."]
|
||||
paths = (".",)
|
||||
|
||||
files = []
|
||||
files: list[str] = []
|
||||
for path in paths:
|
||||
if not os.path.exists(path):
|
||||
click.echo(f"Path {path} does not exist")
|
||||
|
Loading…
Reference in New Issue
Block a user