1111
1212from functools import wraps
1313from collections import namedtuple
14- from typing import Dict , List , Sequence , Tuple , Union , Callable
14+ from typing import Dict , List , Sequence , Tuple , Optional , Union , Callable
1515
1616from shamir_mnemonic import generate_mnemonics
1717
2020
2121from .defaults import BITS_DEFAULT , BITS , MNEM_ROWS_COLS , GROUP_REQUIRED_RATIO , CRYPTO_PATHS
2222from .util import ordinal
23+ from .recovery import produce_bip39 , recover_bip39
2324
2425log = logging .getLogger ( __package__ )
2526
@@ -72,7 +73,7 @@ def path_edit(
7273 if edit .startswith ( '.' ):
7374 new_segs = edit .lstrip ( './' ).split ( '/' )
7475 cur_segs = path .split ( '/' )
75- log .info ( f"Using { edit } to replace last { len (new_segs )} of { path } with { '/' .join (new_segs )} " )
76+ log .debug ( f"Using { edit } to replace last { len (new_segs )} of { path } with { '/' .join (new_segs )} " )
7677 if len ( new_segs ) >= len ( cur_segs ):
7778 raise ValueError ( f"Cannot use { edit } to replace last { len (new_segs )} of { path } with { '/' .join (new_segs )} " )
7879 res_segs = cur_segs [:len (cur_segs )- len (new_segs )] + new_segs
@@ -267,7 +268,7 @@ class Account:
267268 | XRP | Legacy | m/44'/144'/0'/0/0 | r... | Beta |
268269
269270 """
270- CRYPTO_NAMES = dict ( # Currently supported (in order of visibility)
271+ CRYPTO_NAMES = dict ( # Currently supported (in order of visibility)
271272 ethereum = 'ETH' ,
272273 bitcoin = 'BTC' ,
273274 litecoin = 'LTC' ,
@@ -371,14 +372,26 @@ def supported( cls, crypto ):
371372 for it, or raises an a ValueError. Eg. "Ethereum" --> "ETH"
372373
373374 """
374- validated = cls .CRYPTO_NAMES .get (
375+ validated = cls .CRYPTO_NAMES .get (
375376 crypto .lower (),
376377 crypto .upper () if crypto .upper () in cls .CRYPTOCURRENCIES else None
377378 )
378379 if validated :
379380 return validated
380381 raise ValueError ( f"{ crypto } not presently supported; specify { ', ' .join ( cls .CRYPTOCURRENCIES )} " )
381382
383+ def __str__ ( self ):
384+ """Until from_seed/from_path are invoked, may not have an address or derivation path."""
385+ address = None
386+ try :
387+ address = self .address
388+ except Exception :
389+ pass
390+ return f"{ self .crypto } : { address } "
391+
392+ def __repr__ ( self ):
393+ return f"{ self .__class__ .__name__ } ({ self } @{ self .path } )"
394+
382395 def __init__ ( self , crypto , format = None ):
383396 crypto = Account .supported ( crypto )
384397 cryptocurrency = self .CRYPTO_LOCAL .get ( crypto )
@@ -416,15 +429,14 @@ def from_path( self, path: str = None ) -> "Account":
416429 """Change the Account to derive from the provided path.
417430
418431 If a partial path is provided (eg "...1'/0/3"), then use it to replace the given segments in
419- current account path, leaving the remainder alone.
432+ current (or the default) account path, leaving the remainder alone.
420433
421434 """
435+ from_path = self .path or Account .path_default ( self .crypto , self .format )
422436 if path :
423- path = path_edit ( self .path , path )
424- else :
425- path = Account .path_default ( self .crypto , self .format )
437+ from_path = path_edit ( from_path , path )
426438 self .hdwallet .clean_derivation ()
427- self .hdwallet .from_path ( path )
439+ self .hdwallet .from_path ( from_path )
428440 return self
429441
430442 @property
@@ -656,15 +668,19 @@ def path_sequence(
656668
657669
658670def cryptopaths_parser ( cryptocurrency , edit = None ):
659- """
660- Generate a standard cryptopaths list, from the given list of "<crypto>[:<paths>]"
661- cryptocurrencies (default: CRYPTO_PATHS).
671+ """Generate a standard cryptopaths list, from the given sequnce of (<crypto>,<paths>) or
672+ "<crypto>[:<paths>]" cryptocurrencies (default: CRYPTO_PATHS).
662673
663- Adjusts the provided derivation paths by an optional eg. "../-" path adjustment."""
674+ Adjusts the provided derivation paths by an optional eg. "../-" path adjustment.
675+
676+ """
664677 cryptopaths = []
665678 for crypto in cryptocurrency or CRYPTO_PATHS :
666679 try :
667- crypto ,paths = crypto .split ( ':' )
680+ if type (crypto ) is str :
681+ crypto ,paths = crypto .split ( ':' ) # A sequence of str
682+ else :
683+ crypto ,paths = crypto # A sequence of tuples
668684 except ValueError :
669685 crypto ,paths = crypto ,None
670686 crypto = Account .supported ( crypto )
@@ -770,33 +786,65 @@ def create(
770786 name : str ,
771787 group_threshold : int ,
772788 groups : Dict [str ,Tuple [int , int ]],
773- master_secret : bytes = None , # Default: 128-bit seeds
789+ master_secret : bytes = None , # Default: 128-bit seeds
774790 passphrase : bytes = b"" ,
791+ using_bip39 : bool = False , # Generate wallet Seed from master_secret Entropy using BIP-39 generation
775792 iteration_exponent : int = 1 ,
776- cryptopaths : Sequence [Tuple [str ,str ]] = None , # default: ETH, BTC at default paths
793+ cryptopaths : Optional [ Sequence [Union [ str , Tuple [str ,str ]] ]] = None , # default: ETH, BTC at default paths
777794 strength : int = 128 ,
778795) -> Tuple [str ,int ,Dict [str ,Tuple [int ,List [str ]]], Sequence [Sequence [Account ]]]:
779- """Creates a SLIP-39 encoding and 1 or more Ethereum accounts. Returns the details, in a form
780- directly compatible with the layout.produce_pdf API.
796+ """Creates a SLIP-39 encoding for supplied master_secret Entropy, and 1 or more Cryptocurrency
797+ accounts. Returns the details, in a form directly compatible with the layout.produce_pdf API.
798+
799+ Creates accountgroups derived from the Seed Entropy. By default, this is done in the SLIP-39
800+ standard, using the master_secret Entropy directly. If a passphrase is supplied, this is also
801+ used in the SLIP-39 standard fashion (not recommended -- not Trezor "Model T" compatible).
802+
803+ If using_bip39, creates the Cryptocurrency accountgroups from the supplied master_secret
804+ Entropy, but by generating the Seed from a BIP-38 Mnemonic produced from the provided entropy
805+ (or generated, default 128 bits), plus the supplied passphrase.
781806
782807 """
808+
783809 if master_secret is None :
784810 assert strength in BITS , f"Invalid { strength } -bit secret length specified"
785811 master_secret = random_secret ( strength // 8 )
812+
786813 g_names ,g_dims = list ( zip ( * groups .items () ))
814+
815+ # Derive the desired account(s) at the specified derivation paths, or the default, using either
816+ # BIP-39 Seed generation, or directly from Entropy for SLIP-39.
817+ if using_bip39 :
818+ # For BIP-39, the passphrase is consumed here, and Cryptocurrency accounts are generated
819+ # using the BIP-39 Seed generated from entropy + passphrase
820+ bip39_mnem = produce_bip39 ( entropy = master_secret )
821+ bip39_seed = recover_bip39 (
822+ mnemonic = bip39_mnem ,
823+ passphrase = passphrase ,
824+ )
825+ accts = list ( accountgroups (
826+ master_secret = bip39_seed ,
827+ cryptopaths = cryptopaths ,
828+ allow_unbounded = False ,
829+ ))
830+ passphrase = b""
831+ else :
832+ # For SLIP-39, accounts are generated directly from supplied Entropy, and passphrase
833+ # encrypts the SLIP-39 Mnemonics, below.
834+ accts = list ( accountgroups (
835+ master_secret = master_secret ,
836+ cryptopaths = cryptopaths ,
837+ allow_unbounded = False ,
838+ ))
839+
840+ # Generate the SLIP-39 Mnemonics representing the supplied
787841 mnems = mnemonics (
788842 group_threshold = group_threshold ,
789843 groups = g_dims ,
790844 master_secret = master_secret ,
791845 passphrase = passphrase ,
792846 iteration_exponent = iteration_exponent
793847 )
794- # Derive the desired account(s) at the specified derivation paths, or the default
795- accts = list ( accountgroups (
796- master_secret = master_secret ,
797- cryptopaths = cryptopaths or [('ETH' ,None ), ('BTC' ,None )],
798- allow_unbounded = False ,
799- ))
800848
801849 groups = {
802850 g_name : (g_of , g_mnems )
@@ -859,6 +907,7 @@ def account(
859907 seed = master_secret ,
860908 path = path ,
861909 )
910+ log .debug ( f"Created { acct } from { len (master_secret )* 8 } -bit seed, at derivation path { path } " )
862911 return acct
863912
864913
@@ -879,7 +928,7 @@ def accounts(
879928
880929def accountgroups (
881930 master_secret : bytes ,
882- cryptopaths : Sequence [Tuple [str ,str ]],
931+ cryptopaths : Optional [ Sequence [Union [ str , Tuple [str ,str ]]]] = None , # Default: ETH, BTC
883932 allow_unbounded : bool = True ,
884933) -> Sequence [Sequence [Account ]]:
885934 """Generate the desired cryptocurrency account(s) at each crypto's given path(s). This is useful
@@ -910,7 +959,7 @@ def accountgroups(
910959 crypto = crypto ,
911960 allow_unbounded = allow_unbounded ,
912961 )
913- for crypto ,paths in cryptopaths
962+ for crypto ,paths in cryptopaths_parser ( cryptopaths )
914963 ])
915964
916965
@@ -947,7 +996,7 @@ def addresses(
947996
948997def addressgroups (
949998 master_secret : bytes ,
950- cryptopaths : Sequence [Tuple [str ,str ]],
999+ cryptopaths : Optional [ Sequence [Union [ str , Tuple [str ,str ]]]] = None , # Default ETH, BTC
9511000 allow_unbounded : bool = True ,
9521001) -> Sequence [str ]:
9531002 """Yields account (<crypto>, <path>, <address>) records for the desired cryptocurrencies at paths.
@@ -960,5 +1009,5 @@ def addressgroups(
9601009 crypto = crypto ,
9611010 allow_unbounded = allow_unbounded ,
9621011 )
963- for crypto ,paths in cryptopaths
1012+ for crypto ,paths in cryptopaths_parser ( cryptopaths )
9641013 ])
0 commit comments