3333from typing import Dict , List , Sequence , Tuple , Optional , Union , Callable
3434
3535from shamir_mnemonic import generate_mnemonics
36+ from shamir_mnemonic .shamir import RANDOM_BYTES
3637
3738import hdwallet
3839from hdwallet import cryptocurrencies
3940
40- from .defaults import BITS_DEFAULT , BITS , MNEM_ROWS_COLS , GROUP_REQUIRED_RATIO , CRYPTO_PATHS
41- from .util import ordinal , commas
41+ from .defaults import (
42+ BITS_DEFAULT , BITS , MNEM_ROWS_COLS , GROUPS , GROUP_REQUIRED_RATIO , GROUP_THRESHOLD_RATIO , CRYPTO_PATHS
43+ )
44+ from .util import ordinal , commas , is_listlike , is_mapping
4245from .recovery import produce_bip39 , recover_bip39 , recover as recover_slip39
4346
4447__author__ = "Perry Kundert"
4851
4952log = logging .getLogger ( __package__ )
5053
54+
5155# Support for private key encryption via BIP-38 and Ethereum JSON wallet is optional; pip install slip39[wallet]
5256paper_wallet_issues = []
5357try :
7377 paper_wallet_issues .append ( message )
7478
7579
76- RANDOM_BYTES = secrets .token_bytes
77-
78-
7980def paper_wallet_available ():
8081 """Determine if encrypted BIP-38 and Ethereum JSON Paper Wallets are available."""
8182 available = AES and scrypt and eth_account
@@ -118,7 +119,7 @@ def path_edit(
118119
119120class BinanceMainnet ( cryptocurrencies .Cryptocurrency ):
120121 NAME = "Binance"
121- SYMBOL = "BNB "
122+ SYMBOL = "BSC "
122123 NETWORK = "mainnet"
123124 SOURCE_CODE = "https://github.com/bnb-chain/bsc"
124125 COIN_TYPE = cryptocurrencies .CoinType ({
@@ -249,7 +250,7 @@ class Account:
249250 | Crypto | Semantic | Path | Address | Support |
250251 |--------+----------+-------------------+---------+---------|
251252 | ETH | Legacy | m/44'/ 60'/0'/0/0 | 0x... | |
252- | BNB | Legacy | m/44'/ 60'/0'/0/0 | 0x... | Beta |
253+ | BSC | Legacy | m/44'/ 60'/0'/0/0 | 0x... | Beta |
253254 | BTC | Legacy | m/44'/ 0'/0'/0/0 | 1... | |
254255 | | SegWit | m/49'/ 0'/0'/0/0 | 3... | |
255256 | | Bech32 | m/84'/ 0'/0'/0/0 | bc1... | |
@@ -267,7 +268,7 @@ class Account:
267268 BTC = 'Bitcoin' ,
268269 LTC = 'Litecoin' ,
269270 DOGE = 'Dogecoin' ,
270- BNB = 'Binance' ,
271+ BSC = 'Binance' ,
271272 XRP = 'Ripple' ,
272273 )
273274 CRYPTO_DECIMALS = dict (
@@ -283,7 +284,7 @@ class Account:
283284 BTC = 24 ,
284285 LTC = 24 ,
285286 DOGE = 24 ,
286- BNB = 18 ,
287+ BSC = 18 ,
287288 XRP = 6 ,
288289 )
289290 CRYPTO_NAMES = dict (
@@ -293,21 +294,21 @@ class Account:
293294 bitcoin = 'BTC' ,
294295 litecoin = 'LTC' ,
295296 dogecoin = 'DOGE' ,
296- binance = 'BNB ' ,
297+ binance = 'BSC ' ,
297298 ripple = 'XRP' ,
298299 )
299300 CRYPTOCURRENCIES = set ( CRYPTO_NAMES .values () )
300- CRYPTOCURRENCIES_BETA = set ( ('BNB ' , 'XRP' ) )
301+ CRYPTOCURRENCIES_BETA = set ( ('BSC ' , 'XRP' ) )
301302
302- ETHJS_ENCRYPT = set ( ('ETH' , 'BNB ' ) ) # Can be encrypted w/ Ethereum JSON wallet
303+ ETHJS_ENCRYPT = set ( ('ETH' , 'BSC ' ) ) # Can be encrypted w/ Ethereum JSON wallet
303304 BIP38_ENCRYPT = CRYPTOCURRENCIES - ETHJS_ENCRYPT # Can be encrypted w/ BIP-38
304305
305306 CRYPTO_FORMAT = dict (
306307 ETH = "legacy" ,
307308 BTC = "bech32" ,
308309 LTC = "bech32" ,
309310 DOGE = "legacy" ,
310- BNB = "legacy" ,
311+ BSC = "legacy" ,
311312 XRP = "legacy" ,
312313 )
313314
@@ -317,11 +318,11 @@ class Account:
317318 XRP = XRPHDWallet ,
318319 )
319320 CRYPTO_LOCAL = dict (
320- BNB = BinanceMainnet ,
321+ BSC = BinanceMainnet ,
321322 XRP = RippleMainnet ,
322323 )
323324 CRYPTO_LOCAL_SYMBOL = dict (
324- BNB = "ETH"
325+ BSC = "ETH"
325326 )
326327
327328 # The available address formats and default derivation paths.
@@ -331,7 +332,7 @@ class Account:
331332 ETH = dict (
332333 legacy = "m/44'/60'/0'/0/0" ,
333334 ),
334- BNB = dict (
335+ BSC = dict (
335336 legacy = "m/44'/60'/0'/0/0" ,
336337 ),
337338 BTC = dict (
@@ -356,7 +357,7 @@ class Account:
356357 ETH = dict (
357358 legacy = "p2pkh" ,
358359 ),
359- BNB = dict (
360+ BSC = dict (
360361 legacy = "p2pkh" ,
361362 ),
362363 BTC = dict (
@@ -444,12 +445,6 @@ def __init__( self, crypto, format=None ):
444445 self .format = format .lower () if format else Account .address_format ( crypto )
445446 semantic = self .CRYPTO_FORMAT_SEMANTIC [crypto ][self .format ]
446447 hdwallet_cls = self .CRYPTO_WALLET_CLS .get ( crypto , hdwallet .HDWallet )
447- # if hdwallet_cls is None and self.format in ("legacy",):
448- # hdwallet_cls = hdwallet.HDWallet # hdwallet.BIP44HDWallet
449- # if hdwallet_cls is None and self.format in ("segwit",):
450- # hdwallet_cls = hdwallet.HDWALLET # hdwallet.BIP49HDWallet
451- # if hdwallet_cls is None and self.format in ("bech32",):
452- # hdwallet_cls = hdwallet.BIP84HDWallet
453448 if hdwallet_cls is None :
454449 raise ValueError ( f"{ crypto } does not support address format { self .format } " )
455450 self .hdwallet = hdwallet_cls ( symbol = crypto , cryptocurrency = cryptocurrency , semantic = semantic )
@@ -894,11 +889,11 @@ def cryptopaths_parser(
894889
895890 cry ,* pth = crypto
896891 pth ,* fmt = pth or (None ,)
897- fmt , = fmt or (None ,)
892+ fmt , = fmt or (format ,)
898893
899894 cry = Account .supported ( cry )
900895 if not pth :
901- pth = Account .path_default ( cry , fmt or format )
896+ pth = Account .path_default ( cry , fmt )
902897 if hardened_defaults :
903898 pth ,_ = path_hardened ( pth )
904899 if edit :
@@ -907,7 +902,7 @@ def cryptopaths_parser(
907902
908903
909904def random_secret (
910- seed_length = BITS_DEFAULT // 8
905+ seed_length : Optional [ int ]
911906) -> bytes :
912907 """Generates a new random secret.
913908
@@ -930,6 +925,8 @@ def random_secret(
930925 2**128 / 1e9 / 7e9 / (60*60*24*365) == 1,541,469,010,115.145
931926
932927 """
928+ assert seed_length , \
929+ f"Must supply a non-zero length in bytes, not { seed_length } "
933930 return RANDOM_BYTES ( seed_length )
934931
935932
@@ -1002,21 +999,32 @@ def group_parser( group_spec ):
1002999
10031000def create (
10041001 name : str ,
1005- group_threshold : int ,
1006- groups : Dict [str ,Tuple [int , int ]],
1007- master_secret : Optional [bytes ] = None , # Default: generate 128-bit Seed Entropy
1002+ group_threshold : Optional [ Union [ int ,float ]] = None , # Default: 1/2 of groups, rounded up
1003+ groups : Optional [ Union [ List [ str ], Dict [str ,Tuple [int , int ]]]] = None , # Default: 4 groups (see defaults.py)
1004+ master_secret : Optional [Union [ str , bytes ]] = None , # Default: generate 128-bit Seed Entropy
10081005 passphrase : Optional [Union [bytes ,str ]] = None ,
1009- using_bip39 : bool = False , # Produce wallet Seed from master_secret Entropy using BIP-39 generation
1006+ using_bip39 : Optional [ bool ] = None , # Produce wallet Seed from master_secret Entropy using BIP-39 generation
10101007 iteration_exponent : int = 1 ,
10111008 cryptopaths : Optional [Sequence [Union [str ,Tuple [str ,str ],Tuple [str ,str ,str ]]]] = None , # default: ETH, BTC at default path, format
1012- strength : int = 128 ,
1009+ strength : Optional [ int ] = None , # Default: 128
10131010) -> Tuple [str ,int ,Dict [str ,Tuple [int ,List [str ]]], Sequence [Sequence [Account ]], bool ]:
10141011 """Creates a SLIP-39 encoding for supplied master_secret Entropy, and 1 or more Cryptocurrency
10151012 accounts. Returns the Details, in a form directly compatible with the layout.produce_pdf API.
10161013
10171014 The master_secret Seed Entropy is discarded (because it is, of course, always recoverable from
10181015 the SLIP-39 mnemonics).
10191016
1017+ We strive to default to "do the right thing", here. If you supply BIP-39 Mnemonics, we'll
1018+ round-trip them via SLIP-39, and produce compatible crypto account addresses. This should be
1019+ the "typical" case, as most people already have BIP-39 Mnemonics. If you don't have a BIP-39
1020+ Mnemonic, make sure you set using_bip39 = True; this *also* implies that you *MUST* have already
1021+ converted your entropy to BIP-39 (or are going to recover it and do so, later), so we'll warn
1022+ you to do that. If a passphrase is provided, it is assumed to be a BIP-39 passphrase, and is used
1023+ to generate the crypto account addresses -- it will *not* be used to "encrypt" the SLIP-39!
1024+
1025+ If you supply raw entropy, we'll assume you have SLIP-39 compatible wallet and want to use it
1026+ directly.
1027+
10201028 Creates accountgroups derived from the Seed Entropy. By default, this is done in the SLIP-39
10211029 standard, using the master_secret Entropy directly. If a passphrase is supplied, this is also
10221030 used in the SLIP-39 standard fashion (not recommended -- not Trezor "Model T" compatible).
@@ -1027,16 +1035,33 @@ def create(
10271035
10281036 """
10291037 if master_secret is None :
1030- assert strength in BITS , f"Invalid { strength } -bit secret length specified"
1038+ if not strength :
1039+ strength = BITS_DEFAULT
10311040 master_secret = random_secret ( strength // 8 )
1032-
1033- g_names ,g_dims = list ( zip ( * groups .items () ))
1041+ if isinstance ( master_secret , bytes ) and not ( 128 <= len ( master_secret ) * 8 <= 512 ):
1042+ log .warning ( f"Strangely sized { len (master_secret ) * 8 } -bit entropy provided; may be weak" )
1043+
1044+ # If a non-hex str is passed as entropy, assume it is a BIP-39 Mnemonic, and that we want to use
1045+ # SLIP-39 to round-trip the underlying BIP-39 entropy AND derive compatible wallets.
1046+ if isinstance ( master_secret , str ) and all ( c in '0123456789abcdef' for c in master_secret .lower () ):
1047+ master_secret = codecs .decode ( master_secret , 'hex_codec' )
1048+ if using_bip39 is None and isinstance ( master_secret , str ):
1049+ # Assume it must be a BIP-39 Mnemonic
1050+ using_bip39 = True
1051+ else :
1052+ # Assume caller knows; default False (use SLIP-39 directly)
1053+ using_bip39 = bool ( using_bip39 )
10341054
10351055 # Derive the desired account(s) at the specified derivation paths, or the default, using either
10361056 # BIP-39 Seed generation, or directly from Entropy for SLIP-39.
10371057 if using_bip39 :
10381058 # For BIP-39, the passphrase is consumed here, and Cryptocurrency accounts are generated
1039- # using the BIP-39 Seed generated from entropy + passphrase
1059+ # using the BIP-39 Seed generated from entropy + passphrase. This should be the "typical"
1060+ # use-case, where someone already has a BIP-39 Mnemonic and/or wants to use a "standard"
1061+ # BIP-39 compatible hardware wallet.
1062+ log .warning ( f"Assuming BIP-39 seed entropy: Ensure you recover and use via a BIP-39 Mnemonic" )
1063+ if isinstance ( master_secret , str ):
1064+ master_secret = recover_bip39 ( mnemonic = master_secret , as_entropy = True )
10401065 bip39_mnem = produce_bip39 ( entropy = master_secret )
10411066 bip39_seed = recover_bip39 (
10421067 mnemonic = bip39_mnem ,
@@ -1053,8 +1078,10 @@ def create(
10531078 passphrase = None # Consumed by BIP-39; not used for SLIP-39 Mnemonics "backup"!
10541079 else :
10551080 # For SLIP-39, accounts are generated directly from supplied Entropy, and passphrase
1056- # encrypts the SLIP-39 Mnemonics, below.
1057- log .info (
1081+ # encrypts the SLIP-39 Mnemonics, below. Using a SLIP-39 with a passphrase is so unlikely
1082+ # to be correct that we will warn about it! You almost *always* want to use SLIP-39
1083+ # *without* a passphase; use eg. Trezor "hidden wallets" instead.
1084+ (log .warning if passphrase else log .info )(
10581085 f"SLIP-39 for { name } from { len (master_secret )* 8 } -bit Entropy directly{ ' w/ SLIP-39 Passphrase' if passphrase else '' } "
10591086 )
10601087 accts = list ( accountgroups (
@@ -1063,6 +1090,15 @@ def create(
10631090 allow_unbounded = False ,
10641091 ))
10651092
1093+ # Deduce groups, using defaults
1094+ if not groups :
1095+ groups = GROUPS
1096+ if not is_mapping ( groups ):
1097+ if isinstance ( groups , str ):
1098+ groups = groups .split ( "," )
1099+ groups = dict ( map ( group_parser , groups ))
1100+ g_names ,g_dims = list ( zip ( * groups .items () ))
1101+
10661102 # Generate the SLIP-39 Mnemonics representing the supplied master_secret Seed Entropy. This
10671103 # always recovers the Seed Entropy; if not using_bip39, this is also the wallet derivation Seed;
10681104 # if using_bip39, the wallet derivation Seed was produced from the BIP-39 Seed generation
@@ -1094,7 +1130,7 @@ def create(
10941130
10951131
10961132def mnemonics (
1097- group_threshold : int ,
1133+ group_threshold : Optional [ int ], # Default: 1/2 of groups, rounded up
10981134 groups : Sequence [Tuple [int , int ]],
10991135 master_secret : Optional [Union [str ,bytes ]] = None ,
11001136 passphrase : Optional [Union [bytes ,str ]] = None ,
@@ -1120,6 +1156,10 @@ def mnemonics(
11201156 f"Only { commas ( BITS , final = 'and' )} -bit seeds supported; { len (master_secret )* 8 } -bit seed supplied" )
11211157 if isinstance ( passphrase , str ):
11221158 passphrase = passphrase .encode ( 'UTF-8' )
1159+
1160+ groups = list ( groups )
1161+ if not group_threshold :
1162+ group_threshold = math .ceil ( len ( groups ) * GROUP_THRESHOLD_RATIO )
11231163 return generate_mnemonics (
11241164 group_threshold = group_threshold ,
11251165 groups = groups ,
0 commit comments