2018-07-27 10:05:09 +00:00
#!/usr/bin/env python3
2022-05-23 14:19:19 +00:00
from __future__ import annotations
2023-04-06 11:41:21 +00:00
import datetime
2018-09-14 13:57:11 +00:00
import fnmatch
2018-07-27 10:05:09 +00:00
import json
2018-08-23 11:05:41 +00:00
import logging
2018-07-27 10:05:09 +00:00
import os
2019-04-18 14:27:27 +00:00
import re
import sys
2018-08-23 11:05:41 +00:00
from collections import defaultdict
2018-07-30 12:45:01 +00:00
from hashlib import sha256
2023-10-18 10:53:23 +00:00
from pathlib import Path
2022-05-23 14:19:19 +00:00
from typing import Any , Callable , Iterator , TextIO , cast
2018-07-27 10:05:09 +00:00
import click
2018-07-30 12:25:53 +00:00
import coin_info
2022-05-23 14:19:19 +00:00
from coin_info import Coin , CoinBuckets , Coins , CoinsInfo , FidoApps , SupportInfo
2018-07-30 12:45:01 +00:00
2022-06-15 10:27:48 +00:00
DEFINITIONS_TIMESTAMP_PATH = (
coin_info . DEFS_DIR / " ethereum " / " released-definitions-timestamp.txt "
)
2023-09-14 17:10:25 +00:00
DEFINITIONS_LATEST_URL = (
" https://raw.githubusercontent.com/trezor/definitions/main/definitions-latest.json "
)
2022-06-15 10:27:48 +00:00
2024-01-19 11:39:34 +00:00
HERE = Path ( __file__ ) . parent . resolve ( )
ROOT = HERE . parent . parent
2018-08-23 11:05:41 +00:00
try :
import termcolor
except ImportError :
termcolor = None
2018-07-27 10:05:09 +00:00
try :
import mako
import mako . template
from munch import Munch
CAN_RENDER = True
except ImportError :
CAN_RENDER = False
try :
import requests
except ImportError :
requests = None
try :
from PIL import Image
2018-07-30 15:28:20 +00:00
2021-05-31 13:11:05 +00:00
CAN_CHECK_ICONS = True
2018-07-27 10:05:09 +00:00
except ImportError :
2021-05-31 13:11:05 +00:00
CAN_CHECK_ICONS = False
2018-07-27 10:05:09 +00:00
2018-08-23 11:05:41 +00:00
# ======= Crayon colors ======
USE_COLORS = False
2022-05-23 14:19:19 +00:00
def crayon (
color : str | None , string : str , bold : bool = False , dim : bool = False
) - > str :
2018-08-23 11:05:41 +00:00
if not termcolor or not USE_COLORS :
return string
else :
if bold :
attrs = [ " bold " ]
elif dim :
attrs = [ " dark " ]
else :
attrs = [ ]
return termcolor . colored ( string , color , attrs = attrs )
2022-05-23 14:19:19 +00:00
def print_log ( level : int , * args : Any , * * kwargs : Any ) - > None :
2018-08-23 11:05:41 +00:00
prefix = logging . getLevelName ( level )
if level == logging . DEBUG :
prefix = crayon ( " blue " , prefix , bold = False )
elif level == logging . INFO :
prefix = crayon ( " blue " , prefix , bold = True )
elif level == logging . WARNING :
prefix = crayon ( " red " , prefix , bold = False )
elif level == logging . ERROR :
prefix = crayon ( " red " , prefix , bold = True )
print ( prefix , * args , * * kwargs )
2018-07-30 12:25:53 +00:00
# ======= Mako management ======
2018-07-27 10:05:09 +00:00
2022-05-23 14:19:19 +00:00
def c_str_filter ( b : Any ) - > str :
2018-07-27 10:05:09 +00:00
if b is None :
return " NULL "
2022-05-23 14:19:19 +00:00
def hexescape ( c : bytes ) - > str :
2021-10-13 13:45:53 +00:00
return rf " \ x { c : 02x } "
2018-07-27 10:05:09 +00:00
if isinstance ( b , bytes ) :
return ' " ' + " " . join ( map ( hexescape , b ) ) + ' " '
else :
return json . dumps ( b )
2022-05-23 14:19:19 +00:00
def black_repr_filter ( val : Any ) - > str :
2018-08-15 15:33:33 +00:00
if isinstance ( val , str ) :
if ' " ' in val :
return repr ( val )
else :
return c_str_filter ( val )
elif isinstance ( val , bytes ) :
return " b " + c_str_filter ( val )
else :
return repr ( val )
2022-05-23 14:19:19 +00:00
def ascii_filter ( s : str ) - > str :
2018-07-27 10:05:09 +00:00
return re . sub ( " [^ - \x7e ] " , " _ " , s )
2023-08-11 15:57:32 +00:00
def utf8_str_filter ( s : str ) - > str :
return ' " ' + repr ( s ) [ 1 : - 1 ] + ' " '
2022-05-23 14:19:19 +00:00
def make_support_filter (
support_info : SupportInfo ,
) - > Callable [ [ str , Coins ] , Iterator [ Coin ] ] :
def supported_on ( device : str , coins : Coins ) - > Iterator [ Coin ] :
2021-09-20 10:37:56 +00:00
return ( c for c in coins if support_info [ c . key ] . get ( device ) )
2018-08-07 11:00:30 +00:00
2018-07-30 15:28:20 +00:00
return supported_on
2018-08-15 15:33:33 +00:00
MAKO_FILTERS = {
2023-08-11 15:57:32 +00:00
" utf8_str " : utf8_str_filter ,
2018-08-15 15:33:33 +00:00
" c_str " : c_str_filter ,
" ascii " : ascii_filter ,
" black_repr " : black_repr_filter ,
}
2018-07-27 10:05:09 +00:00
2022-05-23 14:19:19 +00:00
def render_file (
2024-09-23 11:44:57 +00:00
src : Path , dst : Path , coins : CoinsInfo , support_info : SupportInfo , models : list [ str ]
2022-05-23 14:19:19 +00:00
) - > None :
2018-08-07 11:00:30 +00:00
""" Renders `src` template into `dst`.
` src ` is a filename , ` dst ` is an open file object .
"""
2023-10-18 10:53:23 +00:00
template = mako . template . Template ( filename = str ( src . resolve ( ) ) )
2023-04-06 11:41:21 +00:00
eth_defs_date = datetime . datetime . fromisoformat (
DEFINITIONS_TIMESTAMP_PATH . read_text ( ) . strip ( )
)
2024-01-19 11:39:34 +00:00
this_file = Path ( src )
2018-07-30 15:28:20 +00:00
result = template . render (
support_info = support_info ,
supported_on = make_support_filter ( support_info ) ,
2023-04-06 11:41:21 +00:00
ethereum_defs_timestamp = int ( eth_defs_date . timestamp ( ) ) ,
2024-01-19 11:39:34 +00:00
THIS_FILE = this_file ,
ROOT = ROOT ,
2018-07-30 15:28:20 +00:00
* * coins ,
2018-08-15 15:33:33 +00:00
* * MAKO_FILTERS ,
2024-09-23 11:44:57 +00:00
ALL_MODELS = models ,
2018-07-30 15:28:20 +00:00
)
2023-10-18 10:53:23 +00:00
dst . write_text ( str ( result ) )
src_stat = src . stat ( )
os . utime ( dst , ns = ( src_stat . st_atime_ns , src_stat . st_mtime_ns ) )
2018-07-27 10:05:09 +00:00
# ====== validation functions ======
2022-05-23 14:19:19 +00:00
def mark_unsupported ( support_info : SupportInfo , coins : Coins ) - > None :
2019-04-29 16:04:19 +00:00
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 ( ) )
2022-05-23 14:19:19 +00:00
def highlight_key ( coin : Coin , color : str ) - > str :
2018-08-23 17:32:28 +00:00
""" Return a colorful string where the SYMBOL part is bold. """
2018-08-23 11:05:41 +00:00
keylist = coin [ " key " ] . split ( " : " )
if keylist [ - 1 ] . isdigit ( ) :
keylist [ - 2 ] = crayon ( color , keylist [ - 2 ] , bold = True )
else :
keylist [ - 1 ] = crayon ( color , keylist [ - 1 ] , bold = True )
key = crayon ( color , " : " . join ( keylist ) )
2021-09-27 10:13:51 +00:00
name = crayon ( None , f " ( { coin [ ' name ' ] } ) " , dim = True )
return f " { key } { name } "
2018-08-23 11:05:41 +00:00
2022-05-23 14:19:19 +00:00
def find_collisions ( coins : Coins , field : str ) - > CoinBuckets :
2018-08-23 11:05:41 +00:00
""" Detects collisions in a given field. Returns buckets of colliding coins. """
2022-05-23 14:19:19 +00:00
collisions : CoinBuckets = defaultdict ( list )
2018-08-23 11:05:41 +00:00
for coin in coins :
2019-11-26 17:11:09 +00:00
values = coin [ field ]
if not isinstance ( values , list ) :
values = [ values ]
for value in values :
collisions [ value ] . append ( coin )
2018-08-23 11:05:41 +00:00
return { k : v for k , v in collisions . items ( ) if len ( v ) > 1 }
2022-05-23 14:19:19 +00:00
def check_eth ( coins : Coins ) - > bool :
2018-09-07 11:17:47 +00:00
check_passed = True
chains = find_collisions ( coins , " chain " )
for key , bucket in chains . items ( ) :
2021-09-27 10:13:51 +00:00
bucket_str = " , " . join ( f " { coin [ ' key ' ] } ( { coin [ ' name ' ] } ) " for coin in bucket )
2018-09-07 11:17:47 +00:00
chain_name_str = " colliding chain name " + crayon ( None , key , bold = True ) + " : "
print_log ( logging . ERROR , chain_name_str , bucket_str )
check_passed = False
return check_passed
2022-05-23 14:19:19 +00:00
def check_btc ( coins : Coins ) - > bool :
2018-07-27 10:05:09 +00:00
check_passed = True
2018-08-23 17:32:28 +00:00
# validate individual coin data
2018-07-27 10:05:09 +00:00
for coin in coins :
2018-07-30 12:25:53 +00:00
errors = coin_info . validate_btc ( coin )
2018-07-27 10:05:09 +00:00
if errors :
check_passed = False
2018-08-23 11:05:41 +00:00
print_log ( logging . ERROR , " invalid definition for " , coin [ " name " ] )
2018-07-27 10:05:09 +00:00
print ( " \n " . join ( errors ) )
2022-05-23 14:19:19 +00:00
def collision_str ( bucket : Coins ) - > str :
2018-08-23 17:32:28 +00:00
""" Generate a colorful string out of a bucket of colliding coins. """
2022-05-23 14:19:19 +00:00
coin_strings : list [ str ] = [ ]
2018-08-23 11:05:41 +00:00
for coin in bucket :
name = coin [ " name " ]
prefix = " "
2022-06-15 10:27:48 +00:00
if coin [ " is_testnet " ] :
2018-08-23 11:05:41 +00:00
color = " green "
elif name == " Bitcoin " :
color = " red "
2019-04-29 16:04:19 +00:00
elif coin [ " unsupported " ] :
2018-08-23 11:05:41 +00:00
color = " grey "
prefix = crayon ( " blue " , " (X) " , bold = True )
else :
color = " blue "
hl = highlight_key ( coin , color )
coin_strings . append ( prefix + hl )
return " , " . join ( coin_strings )
2022-05-23 14:19:19 +00:00
def print_collision_buckets (
buckets : CoinBuckets ,
prefix : str ,
maxlevel : int = logging . ERROR ,
strict : bool = False ,
) - > bool :
2018-08-23 17:32:28 +00:00
""" Intelligently print collision buckets.
For each bucket , if there are any collision with a mainnet , print it .
If the collision is with unsupported networks or testnets , it ' s just INFO.
If the collision is with supported mainnets , it ' s WARNING.
If the collision with any supported network includes Bitcoin , it ' s an ERROR.
"""
2018-08-23 11:05:41 +00:00
failed = False
for key , bucket in buckets . items ( ) :
2022-06-15 10:27:48 +00:00
mainnets = [ c for c in bucket if not c [ " is_testnet " ] ]
2018-08-23 11:05:41 +00:00
2019-04-29 16:04:19 +00:00
have_bitcoin = any ( coin [ " name " ] == " Bitcoin " for coin in mainnets )
supported_mainnets = [ c for c in mainnets if not c [ " unsupported " ] ]
supported_networks = [ c for c in bucket if not c [ " unsupported " ] ]
2018-08-23 11:05:41 +00:00
if len ( mainnets ) > 1 :
2018-12-17 22:11:30 +00:00
if ( have_bitcoin or strict ) and len ( supported_networks ) > 1 :
2018-08-23 17:09:35 +00:00
# ANY collision with Bitcoin is bad
2018-08-28 11:56:53 +00:00
level = maxlevel
2018-08-23 17:09:35 +00:00
failed = True
elif len ( supported_mainnets ) > 1 :
# collision between supported networks is still pretty bad
level = logging . WARNING
2018-08-23 11:05:41 +00:00
else :
2018-08-23 17:09:35 +00:00
# collision between some unsupported networks is OK
2018-08-23 11:05:41 +00:00
level = logging . INFO
2021-09-27 10:13:51 +00:00
print_log ( level , f " { prefix } { key } : " , collision_str ( bucket ) )
2018-08-23 11:05:41 +00:00
return failed
# slip44 collisions
2018-12-17 22:11:30 +00:00
print ( " Checking SLIP44 values collisions... " )
2018-09-07 11:17:47 +00:00
slip44 = find_collisions ( coins , " slip44 " )
2018-12-17 22:11:30 +00:00
if print_collision_buckets ( slip44 , " value " , strict = True ) :
2018-08-23 11:05:41 +00:00
check_passed = False
2018-08-23 17:32:28 +00:00
# only check address_type on coins that don't use cashaddr
2018-08-23 11:05:41 +00:00
nocashaddr = [ coin for coin in coins if not coin . get ( " cashaddr_prefix " ) ]
2018-08-28 11:56:53 +00:00
2018-08-23 11:05:41 +00:00
print ( " Checking address_type collisions... " )
2018-09-07 11:17:47 +00:00
address_type = find_collisions ( nocashaddr , " address_type " )
2018-08-23 11:05:41 +00:00
if print_collision_buckets ( address_type , " address type " ) :
check_passed = False
print ( " Checking address_type_p2sh collisions... " )
2018-09-07 11:17:47 +00:00
address_type_p2sh = find_collisions ( nocashaddr , " address_type_p2sh " )
2018-08-23 11:05:41 +00:00
# we ignore failed checks on P2SH, because reasons
2018-08-28 11:56:53 +00:00
print_collision_buckets ( address_type_p2sh , " address type " , logging . WARNING )
2018-07-27 10:05:09 +00:00
2019-01-16 12:52:18 +00:00
print ( " Checking genesis block collisions... " )
genesis = find_collisions ( coins , " hash_genesis_block " )
print_collision_buckets ( genesis , " genesis block " , logging . WARNING )
2018-07-27 10:05:09 +00:00
return check_passed
2022-06-15 10:27:48 +00:00
def check_dups ( buckets : CoinBuckets ) - > bool :
2018-08-23 17:32:28 +00:00
""" Analyze and pretty-print results of `coin_info.mark_duplicate_shortcuts`.
The results are buckets of colliding symbols .
If the collision is only between ERC20 tokens , it ' s DEBUG.
If the collision includes one non - token , it ' s INFO.
If the collision includes more than one non - token , it ' s ERROR and printed always.
"""
2018-08-28 11:56:53 +00:00
2022-05-23 14:19:19 +00:00
def coin_str ( coin : Coin ) - > str :
2022-06-15 10:27:48 +00:00
""" Colorize coins according to support / override status. """
2019-04-29 16:04:19 +00:00
prefix = " "
if coin [ " unsupported " ] :
color = " grey "
prefix = crayon ( " blue " , " (X) " , bold = True )
2018-08-23 11:05:41 +00:00
else :
color = " red "
2019-04-29 16:04:19 +00:00
2018-08-23 11:05:41 +00:00
if not coin . get ( " duplicate " ) :
2019-04-29 16:04:19 +00:00
prefix = crayon ( " green " , " * " , bold = True ) + prefix
highlighted = highlight_key ( coin , color )
2021-09-27 10:13:51 +00:00
return f " { prefix } { highlighted } "
2018-08-23 11:05:41 +00:00
2018-08-15 17:20:15 +00:00
check_passed = True
2018-08-23 11:05:41 +00:00
for symbol in sorted ( buckets . keys ( ) ) :
bucket = buckets [ symbol ]
if not bucket :
2018-08-15 17:20:15 +00:00
continue
2018-08-23 11:05:41 +00:00
2022-06-15 10:27:48 +00:00
# supported coins from the bucket
2019-04-29 16:04:19 +00:00
supported = [ coin for coin in bucket if not coin [ " unsupported " ] ]
2018-08-23 11:05:41 +00:00
# string generation
dup_str = " , " . join ( coin_str ( coin ) for coin in bucket )
2022-06-15 10:27:48 +00:00
if any ( coin . get ( " duplicate " ) for coin in supported ) :
# At least one supported coin is marked as duplicate.
2018-08-23 11:05:41 +00:00
level = logging . ERROR
2018-08-15 17:20:15 +00:00
check_passed = False
2019-04-29 16:04:19 +00:00
elif len ( supported ) > 1 :
2022-06-15 10:27:48 +00:00
# More than one supported coin in bucket, but no marked duplicates
# --> all must have been cleared by an override.
level = logging . INFO
2019-04-29 16:04:19 +00:00
else :
2022-06-15 10:27:48 +00:00
# At most 1 supported coin in bucket. This is OK.
2019-04-29 16:04:19 +00:00
level = logging . DEBUG
2018-08-15 17:20:15 +00:00
2018-08-23 11:05:41 +00:00
if symbol == " _override " :
print_log ( level , " force-set duplicates: " , dup_str )
else :
2021-09-27 10:13:51 +00:00
print_log ( level , f " duplicate symbol { symbol . upper ( ) } : " , dup_str )
2018-08-23 11:05:41 +00:00
2018-08-15 17:20:15 +00:00
return check_passed
2022-05-23 14:19:19 +00:00
def check_backends ( coins : Coins ) - > bool :
2018-07-27 10:05:09 +00:00
check_passed = True
for coin in coins :
genesis_block = coin . get ( " hash_genesis_block " )
if not genesis_block :
continue
backends = coin . get ( " blockbook " , [ ] ) + coin . get ( " bitcore " , [ ] )
for backend in backends :
print ( " checking " , backend , " ... " , end = " " , flush = True )
try :
2022-05-23 14:19:19 +00:00
assert requests is not None
2018-07-30 12:25:53 +00:00
j = requests . get ( backend + " /api/block-index/0 " ) . json ( )
2018-07-27 10:05:09 +00:00
if j [ " blockHash " ] != genesis_block :
raise RuntimeError ( " genesis block mismatch " )
except Exception as e :
print ( e )
check_passed = False
else :
print ( " OK " )
return check_passed
2022-05-23 14:19:19 +00:00
def check_icons ( coins : Coins ) - > bool :
2018-07-30 12:45:01 +00:00
check_passed = True
for coin in coins :
key = coin [ " key " ]
icon_file = coin . get ( " icon " )
if not icon_file :
print ( key , " : missing icon " )
check_passed = False
continue
try :
icon = Image . open ( icon_file )
except Exception :
print ( key , " : failed to open icon file " , icon_file )
check_passed = False
continue
if icon . size != ( 96 , 96 ) or icon . mode != " RGBA " :
print ( key , " : bad icon format (must be RGBA 96x96) " )
check_passed = False
return check_passed
2022-06-15 10:27:48 +00:00
IGNORE_NONUNIFORM_KEYS = frozenset ( ( " unsupported " , " duplicate " , " coingecko_id " ) )
2018-08-23 17:09:54 +00:00
2022-05-23 14:19:19 +00:00
def check_key_uniformity ( coins : Coins ) - > bool :
keysets : dict [ frozenset [ str ] , Coins ] = defaultdict ( list )
2018-08-23 17:09:54 +00:00
for coin in coins :
keyset = frozenset ( coin . keys ( ) ) | IGNORE_NONUNIFORM_KEYS
keysets [ keyset ] . append ( coin )
if len ( keysets ) < = 1 :
return True
buckets = list ( keysets . values ( ) )
buckets . sort ( key = lambda x : len ( x ) )
majority = buckets [ - 1 ]
rest = sum ( buckets [ : - 1 ] , [ ] )
2018-11-20 16:51:04 +00:00
reference_keyset = set ( majority [ 0 ] . keys ( ) ) | IGNORE_NONUNIFORM_KEYS
print ( reference_keyset )
2018-08-23 17:09:54 +00:00
for coin in rest :
key = coin [ " key " ]
2018-11-20 16:51:04 +00:00
keyset = set ( coin . keys ( ) ) | IGNORE_NONUNIFORM_KEYS
2018-08-23 17:09:54 +00:00
missing = " , " . join ( reference_keyset - keyset )
if missing :
2021-09-27 10:13:51 +00:00
print_log ( logging . ERROR , f " coin { key } has missing keys: { missing } " )
2018-08-23 17:09:54 +00:00
additional = " , " . join ( keyset - reference_keyset )
if additional :
2018-11-29 14:42:49 +00:00
print_log (
logging . ERROR ,
2021-09-27 10:13:51 +00:00
f " coin { key } has superfluous keys: { additional } " ,
2018-11-29 14:42:49 +00:00
)
2018-08-23 17:09:54 +00:00
return False
2022-05-23 14:19:19 +00:00
def check_segwit ( coins : Coins ) - > bool :
2019-02-12 09:56:34 +00:00
for coin in coins :
segwit = coin [ " segwit " ]
2019-03-26 11:22:57 +00:00
segwit_fields = [
" bech32_prefix " ,
" xpub_magic_segwit_native " ,
" xpub_magic_segwit_p2sh " ,
2021-01-13 21:58:07 +00:00
" xpub_magic_multisig_segwit_native " ,
" xpub_magic_multisig_segwit_p2sh " ,
2019-03-26 11:22:57 +00:00
]
2019-02-12 09:56:34 +00:00
if segwit :
2019-03-26 11:22:57 +00:00
for field in segwit_fields :
if coin [ field ] is None :
print_log (
logging . ERROR ,
coin [ " name " ] ,
2021-09-27 10:13:51 +00:00
f " segwit is True => { field } should be set " ,
2019-03-26 11:22:57 +00:00
)
return False
2019-02-12 09:56:34 +00:00
else :
2019-03-26 11:22:57 +00:00
for field in segwit_fields :
if coin [ field ] is not None :
print_log (
logging . ERROR ,
coin [ " name " ] ,
2021-09-27 10:13:51 +00:00
f " segwit is False => { field } should NOT be set " ,
2019-03-26 11:22:57 +00:00
)
return False
2019-02-12 09:56:34 +00:00
return True
2019-11-26 17:11:09 +00:00
FIDO_KNOWN_KEYS = frozenset (
2019-12-11 10:47:02 +00:00
(
" key " ,
" u2f " ,
" webauthn " ,
2020-07-30 10:11:26 +00:00
" name " ,
2019-12-11 10:47:02 +00:00
" use_sign_count " ,
" use_self_attestation " ,
2023-03-10 13:52:50 +00:00
" use_compact " ,
2019-12-11 10:47:02 +00:00
" no_icon " ,
" icon " ,
)
2019-11-26 17:11:09 +00:00
)
2022-05-23 14:19:19 +00:00
def check_fido ( apps : FidoApps ) - > bool :
2019-11-26 17:11:09 +00:00
check_passed = True
2020-07-30 10:11:26 +00:00
u2fs = find_collisions ( ( u for a in apps if " u2f " in a for u in a [ " u2f " ] ) , " app_id " )
for key , bucket in u2fs . items ( ) :
bucket_str = " , " . join ( u2f [ " label " ] for u2f in bucket )
app_id_str = " colliding U2F app ID " + crayon ( None , key , bold = True ) + " : "
print_log ( logging . ERROR , app_id_str , bucket_str )
2019-11-26 17:11:09 +00:00
check_passed = False
webauthn_domains = find_collisions ( ( a for a in apps if " webauthn " in a ) , " webauthn " )
for key , bucket in webauthn_domains . items ( ) :
bucket_str = " , " . join ( app [ " key " ] for app in bucket )
webauthn_str = " colliding WebAuthn domain " + crayon ( None , key , bold = True ) + " : "
print_log ( logging . ERROR , webauthn_str , bucket_str )
check_passed = False
2022-05-23 14:19:19 +00:00
domain_hashes : dict [ bytes , str ] = { }
2020-07-30 10:16:49 +00:00
for app in apps :
if " webauthn " in app :
for domain in app [ " webauthn " ] :
domain_hashes [ sha256 ( domain . encode ( ) ) . digest ( ) ] = domain
for app in apps :
if " u2f " in app :
for u2f in app [ " u2f " ] :
domain = domain_hashes . get ( bytes . fromhex ( u2f [ " app_id " ] ) )
if domain :
print_log (
logging . ERROR ,
" colliding WebAuthn domain "
+ crayon ( None , domain , bold = True )
+ " and U2F app_id "
+ crayon ( None , u2f [ " app_id " ] , bold = True )
+ " for "
+ u2f [ " label " ] ,
)
check_passed = False
2019-11-26 17:11:09 +00:00
for app in apps :
2020-07-30 10:11:26 +00:00
if " name " not in app :
print_log ( logging . ERROR , app [ " key " ] , " : missing name " )
2019-11-26 17:11:09 +00:00
check_passed = False
2020-07-30 10:11:26 +00:00
if " u2f " in app :
for u2f in app [ " u2f " ] :
if " app_id " not in u2f :
print_log ( logging . ERROR , app [ " key " ] , " : missing app_id " )
check_passed = False
if " label " not in u2f :
print_log ( logging . ERROR , app [ " key " ] , " : missing label " )
check_passed = False
2019-11-26 17:11:09 +00:00
if not app . get ( " u2f " ) and not app . get ( " webauthn " ) :
print_log ( logging . ERROR , app [ " key " ] , " : no U2F nor WebAuthn addresses " )
check_passed = False
unknown_keys = set ( app . keys ( ) ) - FIDO_KNOWN_KEYS
if unknown_keys :
print_log ( logging . ERROR , app [ " key " ] , " : unrecognized keys: " , unknown_keys )
2019-11-28 13:00:13 +00:00
check_passed = False
2019-11-26 17:11:09 +00:00
# check icons
2019-11-28 13:00:13 +00:00
if app [ " icon " ] is None :
2019-12-09 14:23:06 +00:00
if app . get ( " no_icon " ) :
continue
print_log ( logging . ERROR , app [ " key " ] , " : missing icon " )
check_passed = False
2019-11-28 13:00:13 +00:00
continue
2019-12-09 14:23:06 +00:00
elif app . get ( " no_icon " ) :
print_log ( logging . ERROR , app [ " key " ] , " : icon present for ' no_icon ' app " )
check_passed = False
2019-11-28 13:00:13 +00:00
try :
icon = Image . open ( app [ " icon " ] )
except Exception :
2019-12-09 14:23:06 +00:00
print_log (
logging . ERROR , app [ " key " ] , " : failed to open icon file " , app [ " icon " ]
)
2019-11-28 13:00:13 +00:00
check_passed = False
2019-11-26 17:11:09 +00:00
continue
if icon . size != ( 128 , 128 ) or icon . mode != " RGBA " :
print_log (
logging . ERROR , app [ " key " ] , " : bad icon format (must be RGBA 128x128) "
)
check_passed = False
2019-12-09 14:23:06 +00:00
2019-11-26 17:11:09 +00:00
return check_passed
2018-07-27 10:05:09 +00:00
# ====== click command handlers ======
@click.group ( )
2018-08-23 11:05:41 +00:00
@click.option (
" --colors/--no-colors " ,
" -c/-C " ,
default = sys . stdout . isatty ( ) ,
help = " Force colored output on/off " ,
)
2022-05-23 14:19:19 +00:00
def cli ( colors : bool ) - > None :
2018-08-23 11:05:41 +00:00
global USE_COLORS
USE_COLORS = colors
2018-07-27 10:05:09 +00:00
@cli.command ( )
2018-08-15 16:46:00 +00:00
# fmt: off
@click.option ( " --backend/--no-backend " , " -b " , default = False , help = " Check blockbook/bitcore responses " )
2018-07-30 15:28:20 +00:00
@click.option ( " --icons/--no-icons " , default = True , help = " Check icon files " )
2018-08-15 16:46:00 +00:00
# fmt: on
2022-06-15 10:27:48 +00:00
def check ( backend : bool , icons : bool ) - > None :
2018-07-27 10:05:09 +00:00
""" Validate coin definitions.
2018-08-23 17:09:54 +00:00
Checks that every btc - like coin is properly filled out , reports duplicate symbols ,
missing or invalid icons , backend responses , and uniform key information - -
i . e . , that all coins of the same type have the same fields in their JSON data .
Uniformity check ignores NEM mosaics and ERC20 tokens , where non - uniformity is
expected .
2018-08-23 11:05:41 +00:00
2022-06-15 10:27:48 +00:00
All shortcut collisions are shown , including colliding ERC20 tokens .
2018-08-23 11:05:41 +00:00
In the output , duplicate ERC tokens will be shown in cyan ; duplicate non - tokens
in red . An asterisk ( * ) next to symbol name means that even though it was detected
as duplicate , it is still included in results .
2018-08-23 17:09:35 +00:00
2018-08-23 17:32:28 +00:00
The collision detection checks that SLIP44 numbers don ' t collide between different
mainnets ( testnet collisions are allowed ) , that ` address_prefix ` doesn ' t collide
with Bitcoin ( other collisions are reported as warnings ) . ` address_prefix_p2sh `
2018-08-23 17:09:35 +00:00
is also checked but we have a bunch of collisions there and can ' t do much
about them , so it ' s not an error.
In the collision checks , Bitcoin is shown in red , other mainnets in blue ,
testnets in green and unsupported networks in gray , marked with ` ( X ) ` for
non - colored output .
2018-07-27 10:05:09 +00:00
"""
2018-07-30 12:45:01 +00:00
if backend and requests is None :
2018-07-27 10:05:09 +00:00
raise click . ClickException ( " You must install requests for backend check " )
2021-05-31 13:11:05 +00:00
if icons and not CAN_CHECK_ICONS :
2018-07-30 12:45:01 +00:00
raise click . ClickException ( " Missing requirements for icon check " )
2018-08-24 13:20:25 +00:00
defs , buckets = coin_info . coin_info_with_duplicates ( )
2019-04-29 16:04:19 +00:00
support_info = coin_info . support_info ( defs )
mark_unsupported ( support_info , defs . as_list ( ) )
2018-07-27 10:05:09 +00:00
all_checks_passed = True
print ( " Checking BTC-like coins... " )
2018-08-24 13:42:06 +00:00
if not check_btc ( defs . bitcoin ) :
2018-07-27 10:05:09 +00:00
all_checks_passed = False
2018-09-07 11:17:47 +00:00
print ( " Checking Ethereum networks... " )
if not check_eth ( defs . eth ) :
all_checks_passed = False
2022-06-15 10:27:48 +00:00
if not check_dups ( buckets ) :
2018-08-15 17:20:15 +00:00
all_checks_passed = False
2018-11-29 14:42:49 +00:00
nontoken_dups = [ coin for coin in defs . as_list ( ) if " dup_key_nontoken " in coin ]
if nontoken_dups :
nontoken_dup_str = " , " . join (
highlight_key ( coin , " red " ) for coin in nontoken_dups
)
print_log ( logging . ERROR , " Non-token duplicate keys: " + nontoken_dup_str )
all_checks_passed = False
2018-07-30 12:45:01 +00:00
if icons :
print ( " Checking icon files... " )
2018-08-24 13:42:06 +00:00
if not check_icons ( defs . bitcoin ) :
2018-07-30 12:45:01 +00:00
all_checks_passed = False
if backend :
2018-07-27 10:05:09 +00:00
print ( " Checking backend responses... " )
2018-08-24 13:42:06 +00:00
if not check_backends ( defs . bitcoin ) :
2018-07-27 10:05:09 +00:00
all_checks_passed = False
2019-02-12 09:56:34 +00:00
print ( " Checking segwit fields... " )
if not check_segwit ( defs . bitcoin ) :
all_checks_passed = False
2018-08-23 17:09:54 +00:00
print ( " Checking key uniformity... " )
for cointype , coinlist in defs . items ( ) :
if cointype in ( " erc20 " , " nem " ) :
continue
if not check_key_uniformity ( coinlist ) :
all_checks_passed = False
2019-11-26 17:11:09 +00:00
print ( " Checking FIDO app definitions... " )
if not check_fido ( coin_info . fido_info ( ) ) :
all_checks_passed = False
2018-07-27 10:05:09 +00:00
if not all_checks_passed :
print ( " Some checks failed. " )
sys . exit ( 1 )
else :
print ( " Everything is OK. " )
2022-05-23 16:46:52 +00:00
type_choice = click . Choice ( [ " bitcoin " , " eth " , " erc20 " , " nem " , " misc " ] )
2023-08-28 14:46:57 +00:00
device_choice = click . Choice ( [ " connect " , " suite " , " T1B1 " , " T2T1 " , " T2B1 " ] )
2022-05-23 16:46:52 +00:00
2018-07-27 10:05:09 +00:00
@cli.command ( )
2018-09-12 16:09:01 +00:00
# fmt: off
2018-09-14 13:57:11 +00:00
@click.option ( " -o " , " --outfile " , type = click . File ( mode = " w " ) , default = " - " )
2018-09-12 16:09:01 +00:00
@click.option ( " -s/-S " , " --support/--no-support " , default = True , help = " Include support data for each coin " )
@click.option ( " -p " , " --pretty " , is_flag = True , help = " Generate nicely formatted JSON " )
2018-10-08 11:57:42 +00:00
@click.option ( " -l " , " --list " , " flat_list " , is_flag = True , help = " Output a flat list of coins " )
2022-05-23 16:46:52 +00:00
@click.option ( " -i " , " --include " , metavar = " FIELD " , multiple = True , help = " Include only these fields (-i shortcut -i name) " )
@click.option ( " -e " , " --exclude " , metavar = " FIELD " , multiple = True , help = " Exclude these fields (-e blockchain_link) " )
@click.option ( " -I " , " --include-type " , metavar = " TYPE " , multiple = True , type = type_choice , help = " Include only these categories (-I bitcoin -I erc20) " )
@click.option ( " -E " , " --exclude-type " , metavar = " TYPE " , multiple = True , type = type_choice , help = " Exclude these categories (-E nem -E misc) " )
@click.option ( " -f " , " --filter " , metavar = " FIELD=FILTER " , multiple = True , help = " Include only coins that match a filter (-f taproot=true -f maintainer= ' *stick* ' ) " )
@click.option ( " -F " , " --filter-exclude " , metavar = " FIELD=FILTER " , multiple = True , help = " Exclude coins that match a filter (-F ' blockbook=[] ' -F ' slip44=* ' ) " )
2018-09-14 13:57:11 +00:00
@click.option ( " -t " , " --exclude-tokens " , is_flag = True , help = " Exclude ERC20 tokens. Equivalent to ' -E erc20 ' " )
2023-08-28 14:46:57 +00:00
@click.option ( " -d " , " --device-include " , metavar = " NAME " , multiple = True , type = device_choice , help = " Only include coins supported on these given devices (-d connect -d T1B1) " )
@click.option ( " -D " , " --device-exclude " , metavar = " NAME " , multiple = True , type = device_choice , help = " Only include coins not supported on these given devices (-D suite -D T2T1) " )
2018-09-12 16:09:01 +00:00
# fmt: on
2018-09-14 13:57:11 +00:00
def dump (
2022-05-23 14:19:19 +00:00
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 ,
2022-05-23 16:46:52 +00:00
device_include : tuple [ str , . . . ] ,
device_exclude : tuple [ str , . . . ] ,
2022-05-23 14:19:19 +00:00
) - > None :
2019-11-26 17:11:09 +00:00
""" Dump coin data in JSON format.
2018-09-04 13:28:06 +00:00
2022-05-23 16:46:52 +00:00
By default prints to stdout , specify an output file with ' -o file.json ' .
2018-09-04 13:28:06 +00:00
This file is structured the same as the internal data . That is , top - level object
is a dict with keys : ' bitcoin ' , ' eth ' , ' erc20 ' , ' nem ' and ' misc ' . Value for each
key is a list of dicts , each describing a known coin .
2018-09-14 13:57:11 +00:00
If ' --list ' is specified , the top - level object is instead a flat list of coins .
2018-09-04 13:28:06 +00:00
\b
Fields are category - specific , except for four common ones :
- ' name ' - human - readable name
- ' shortcut ' - currency symbol
- ' key ' - unique identifier , e . g . , ' bitcoin:BTC '
- ' support ' - a dict with entries per known device
2018-09-12 16:09:01 +00:00
To control the size and properties of the resulting file , you can specify whether
2018-09-14 13:57:11 +00:00
or not you want pretty - printing and whether or not to include support data with
each coin .
2018-09-12 16:09:01 +00:00
2018-09-14 13:57:11 +00:00
You can specify which categories and which fields will be included or excluded .
You cannot specify both include and exclude at the same time . Include is " stronger "
than exclude , in that _only_ the specified fields are included .
You can also specify filters , in the form ' -f field=value ' ( or ' -F ' for inverse
filter ) . Filter values are case - insensitive and support shell - style wildcards ,
so ' -f name=bit* ' finds all coins whose names start with " bit " or " Bit " .
2022-05-23 16:46:52 +00:00
Also devices can be used as filters . For example to find out which coins are
supported in Suite and connect but not on Trezor 1 , it is possible to say
2023-08-28 14:46:57 +00:00
' -d suite -d connect -D T1B1 ' .
2018-09-04 13:28:06 +00:00
"""
2018-09-14 13:57:11 +00:00
if exclude_tokens :
2022-05-23 16:46:52 +00:00
exclude_type + = ( " erc20 " , )
2018-09-14 13:57:11 +00:00
if include and exclude :
raise click . ClickException (
" You cannot specify --include and --exclude at the same time. "
)
if include_type and exclude_type :
raise click . ClickException (
" You cannot specify --include-type and --exclude-type at the same time. "
)
2022-05-23 16:46:52 +00:00
# getting initial info
2018-09-04 13:28:06 +00:00
coins = coin_info . coin_info ( )
2018-09-14 13:57:11 +00:00
support_info = coin_info . support_info ( coins . as_list ( ) )
2018-09-04 13:28:06 +00:00
2022-05-23 16:46:52 +00:00
# optionally adding support info
2018-09-12 16:09:01 +00:00
if support :
for category in coins . values ( ) :
for coin in category :
coin [ " support " ] = support_info [ coin [ " key " ] ]
2018-09-04 13:28:06 +00:00
2018-09-14 13:57:11 +00:00
# filter types
if include_type :
coins_dict = { k : v for k , v in coins . items ( ) if k in include_type }
2022-05-23 16:46:52 +00:00
elif exclude_type :
2018-09-14 13:57:11 +00:00
coins_dict = { k : v for k , v in coins . items ( ) if k not in exclude_type }
2022-05-23 16:46:52 +00:00
else :
coins_dict = coins
2018-09-14 13:57:11 +00:00
# filter individual coins
include_filters = [ f . split ( " = " , maxsplit = 1 ) for f in filter ]
exclude_filters = [ f . split ( " = " , maxsplit = 1 ) for f in filter_exclude ]
# always exclude 'address_bytes', not encodable in JSON
exclude + = ( " address_bytes " , )
2022-05-23 14:19:19 +00:00
def should_include_coin ( coin : Coin ) - > bool :
2018-09-14 13:57:11 +00:00
for field , filter in include_filters :
if field not in coin :
return False
2022-05-23 16:46:52 +00:00
if not fnmatch . fnmatch ( str ( coin [ field ] ) . lower ( ) , filter . lower ( ) ) :
2018-09-14 13:57:11 +00:00
return False
for field , filter in exclude_filters :
if field not in coin :
continue
2022-05-23 16:46:52 +00:00
if fnmatch . fnmatch ( str ( coin [ field ] ) . lower ( ) , filter . lower ( ) ) :
return False
if device_include :
is_supported_everywhere = all (
support_info [ coin [ " key " ] ] . get ( device ) for device in device_include
)
if not is_supported_everywhere :
2018-09-14 13:57:11 +00:00
return False
2022-05-23 16:46:52 +00:00
if device_exclude :
is_supported_somewhere = any (
support_info [ coin [ " key " ] ] . get ( device ) for device in device_exclude
)
if is_supported_somewhere :
2018-09-14 13:57:11 +00:00
return False
return True
2018-09-12 16:09:01 +00:00
2022-05-23 14:19:19 +00:00
def modify_coin ( coin : Coin ) - > Coin :
2018-09-14 13:57:11 +00:00
if include :
2022-05-23 14:19:19 +00:00
return cast ( Coin , { k : v for k , v in coin . items ( ) if k in include } )
2018-09-14 13:57:11 +00:00
else :
2022-05-23 14:19:19 +00:00
return cast ( Coin , { k : v for k , v in coin . items ( ) if k not in exclude } )
2018-09-14 13:57:11 +00:00
for key , coinlist in coins_dict . items ( ) :
coins_dict [ key ] = [ modify_coin ( c ) for c in coinlist if should_include_coin ( c ) ]
2022-05-23 16:46:52 +00:00
# deciding the output structure
2018-09-14 13:57:11 +00:00
if flat_list :
output = sum ( coins_dict . values ( ) , [ ] )
else :
output = coins_dict
2018-07-27 10:05:09 +00:00
2022-05-23 16:46:52 +00:00
# dump the data - to stdout or to a file
2018-07-27 10:05:09 +00:00
with outfile :
2018-09-14 13:57:11 +00:00
indent = 4 if pretty else None
json . dump ( output , outfile , indent = indent , sort_keys = True )
outfile . write ( " \n " )
2018-07-30 12:25:53 +00:00
2018-07-27 10:05:09 +00:00
@cli.command ( )
2018-08-15 16:46:00 +00:00
# fmt: off
2023-10-18 10:53:23 +00:00
@click.argument ( " paths " , type = click . Path ( path_type = Path ) , metavar = " [path]... " , nargs = - 1 )
@click.option ( " -o " , " --outfile " , type = click . Path ( dir_okay = False , writable = True , path_type = Path ) , help = " Alternate output file " )
2018-08-07 11:00:30 +00:00
@click.option ( " -v " , " --verbose " , is_flag = True , help = " Print rendered file names " )
2019-04-27 14:15:35 +00:00
@click.option ( " -b " , " --bitcoin-only " , is_flag = True , help = " Accept only Bitcoin coins " )
2024-09-23 11:44:57 +00:00
@click.option ( " -M " , " --model-exclude " , metavar = " NAME " , multiple = True , type = device_choice , help = " Skip generation for this models (-M T1B1) " )
2018-08-15 16:46:00 +00:00
# fmt: on
2022-05-23 14:19:19 +00:00
def render (
2024-09-23 11:44:57 +00:00
paths : tuple [ Path , . . . ] ,
outfile : Path ,
verbose : bool ,
bitcoin_only : bool ,
model_exclude : tuple [ str , . . . ] ,
2022-05-23 14:19:19 +00:00
) - > None :
2018-08-07 11:00:30 +00:00
""" Generate source code from Mako templates.
2018-07-27 10:05:09 +00:00
2018-08-07 11:00:30 +00:00
For every " foo.bar.mako " filename passed , runs the template and
saves the result as " foo.bar " . For every directory name passed ,
processes all " .mako " files found in that directory .
2018-07-27 10:05:09 +00:00
2018-08-07 11:00:30 +00:00
If ` - o ` is specified , renders a single file into the specified outfile .
2018-07-27 10:05:09 +00:00
If no arguments are given , processes the current directory .
"""
if not CAN_RENDER :
raise click . ClickException ( " Please install ' mako ' and ' munch ' " )
2018-08-07 11:00:30 +00:00
if outfile and ( len ( paths ) != 1 or not os . path . isfile ( paths [ 0 ] ) ) :
raise click . ClickException ( " Option -o can only be used with single input file " )
2018-07-27 10:05:09 +00:00
2018-08-07 11:00:30 +00:00
# prepare defs
2018-08-24 13:20:25 +00:00
defs = coin_info . coin_info ( )
2019-11-28 13:00:13 +00:00
defs [ " fido " ] = coin_info . fido_info ( )
2018-08-15 15:34:20 +00:00
support_info = coin_info . support_info ( defs )
2018-07-27 10:05:09 +00:00
2019-04-27 14:15:35 +00:00
if bitcoin_only :
defs [ " bitcoin " ] = [
x
for x in defs [ " bitcoin " ]
if x [ " coin_name " ] in ( " Bitcoin " , " Testnet " , " Regtest " )
]
2018-07-30 12:45:01 +00:00
# munch dicts - make them attribute-accessible
2018-07-27 10:05:09 +00:00
for key , value in defs . items ( ) :
defs [ key ] = [ Munch ( coin ) for coin in value ]
for key , value in support_info . items ( ) :
support_info [ key ] = Munch ( value )
2023-10-18 10:53:23 +00:00
def do_render ( src : Path , dst : Path ) - > None :
2024-09-23 11:44:57 +00:00
models = coin_info . get_models ( )
models = [ m for m in models if m not in model_exclude ]
2018-08-07 11:00:30 +00:00
if verbose :
2022-05-23 14:19:19 +00:00
click . echo ( f " Rendering { src } => { dst . name } " )
2024-09-23 11:44:57 +00:00
render_file ( src , dst , defs , support_info , models )
2018-08-07 11:00:30 +00:00
# single in-out case
if outfile :
do_render ( paths [ 0 ] , outfile )
return
# find files in directories
if not paths :
2023-10-18 10:53:23 +00:00
paths = ( Path ( ) , )
2018-08-07 11:00:30 +00:00
2023-10-18 10:53:23 +00:00
files : list [ Path ] = [ ]
2018-08-07 11:00:30 +00:00
for path in paths :
2023-10-18 10:53:23 +00:00
if not path . exists ( ) :
2021-09-27 10:13:51 +00:00
click . echo ( f " Path { path } does not exist " )
2023-10-18 10:53:23 +00:00
elif path . is_dir ( ) :
files . extend ( path . glob ( " *.mako " ) )
2018-08-07 11:00:30 +00:00
else :
files . append ( path )
# render each file
2018-07-27 10:05:09 +00:00
for file in files :
2023-10-18 10:53:23 +00:00
if not file . suffix == " .mako " :
2021-09-27 10:13:51 +00:00
click . echo ( f " File { file } does not end with .mako " )
2018-07-27 10:05:09 +00:00
else :
2023-10-18 10:53:23 +00:00
do_render ( file , file . parent / file . stem )
2018-07-27 10:05:09 +00:00
2023-09-14 17:10:25 +00:00
@cli.command ( )
# fmt: off
@click.option ( " -v " , " --verbose " , is_flag = True , help = " Print timestamp and merkle root " )
# fmt: on
def new_definitions ( verbose : bool ) - > None :
""" Update timestamp of external coin definitions. """
assert requests is not None
eth_defs = requests . get ( DEFINITIONS_LATEST_URL ) . json ( )
eth_defs_date = eth_defs [ " metadata " ] [ " datetime " ]
if verbose :
click . echo (
f " Latest definitions from { eth_defs_date } : { eth_defs [ ' metadata ' ] [ ' merkle_root ' ] } "
)
eth_defs_date = datetime . datetime . fromisoformat ( eth_defs_date )
DEFINITIONS_TIMESTAMP_PATH . write_text (
eth_defs_date . isoformat ( timespec = " seconds " ) + " \n "
)
2018-07-27 10:05:09 +00:00
if __name__ == " __main__ " :
cli ( )