|
24 | 24 | import math |
25 | 25 | import re |
26 | 26 | import secrets |
| 27 | +import string |
27 | 28 | import warnings |
28 | 29 |
|
29 | 30 | from functools import wraps |
@@ -442,18 +443,58 @@ def from_mnemonic( self, mnemonic: str, path: str = None ) -> "Account": |
442 | 443 | self.from_path( path ) |
443 | 444 | return self |
444 | 445 |
|
| 446 | + def from_xpubkey( self, xpubkey: str, path: str = None ) -> "Account": |
| 447 | + """Derive the Account from the supplied xpubkey and (optionally) path; uses default |
| 448 | + derivation path for the Account address format, if None provided. |
| 449 | +
|
| 450 | + Since this xpubkey may have been generated at an arbitrary path, eg. |
| 451 | +
|
| 452 | + m/44'/60'/0' |
| 453 | +
|
| 454 | + any subsequent path provided, such as "m/0/0" will give us the address at |
| 455 | + effective path: |
| 456 | +
|
| 457 | + m/44'/60'/0'/0/0 |
| 458 | +
|
| 459 | + However, if we ask for the path from this account, it will return: |
| 460 | +
|
| 461 | + m/0/0 |
| 462 | +
|
| 463 | + It is impossible to correctly recover any "hardened" accounts from an xpubkey, such as |
| 464 | + "m/1'/0". These would need access to the private key material, which is missing. |
| 465 | + Therefore, the original account (or an xprivkey) would be required to access the desired |
| 466 | + path: |
| 467 | +
|
| 468 | + m/44'/60'/0'/1'/0 |
| 469 | +
|
| 470 | + """ |
| 471 | + self.hdwallet.from_xpublic_key( xpubkey ) |
| 472 | + self.from_path( path ) |
| 473 | + return self |
| 474 | + |
| 475 | + def from_xprvkey( self, xprvkey: str, path: str = None ) -> "Account": |
| 476 | + self.hdwallet.from_xpublic_key( xprvkey ) |
| 477 | + self.from_path( path ) |
| 478 | + return self |
| 479 | + |
445 | 480 | def from_path( self, path: str = None ) -> "Account": |
446 | 481 | """Change the Account to derive from the provided path. |
447 | 482 |
|
448 | 483 | If a partial path is provided (eg "...1'/0/3"), then use it to replace the given segments in |
449 | 484 | current (or the default) account path, leaving the remainder alone. |
450 | 485 |
|
| 486 | + If the derivation path is empty (only "m/") then leave the Account at clean_derivation state |
| 487 | +
|
451 | 488 | """ |
452 | 489 | from_path = self.path or Account.path_default( self.crypto, self.format ) |
453 | 490 | if path: |
454 | 491 | from_path = path_edit( from_path, path ) |
455 | 492 | self.hdwallet.clean_derivation() |
456 | | - self.hdwallet.from_path( from_path ) |
| 493 | + # Valid DH wallet derivation paths always start with "m/" |
| 494 | + if not ( from_path and len( from_path ) >= 2 and from_path.startswith( "m/" ) ): |
| 495 | + raise ValueError( f"Unrecognized HD wallet derivation path: {from_path!r}" ) |
| 496 | + if len( from_path ) > 2: |
| 497 | + self.hdwallet.from_path( from_path ) |
457 | 498 | return self |
458 | 499 |
|
459 | 500 | @property |
@@ -515,10 +556,12 @@ def path( self ): |
515 | 556 | @property |
516 | 557 | def key( self ): |
517 | 558 | return self.hdwallet.private_key() |
| 559 | + prvkey = key |
518 | 560 |
|
519 | 561 | @property |
520 | 562 | def xkey( self ): |
521 | 563 | return self.hdwallet.xprivate_key() |
| 564 | + xprvkey = xkey |
522 | 565 |
|
523 | 566 | @property |
524 | 567 | def pubkey( self ): |
@@ -950,15 +993,43 @@ def account( |
950 | 993 | """Generate an HD wallet Account from the supplied master_secret seed, at the given HD derivation |
951 | 994 | path, for the specified cryptocurrency. |
952 | 995 |
|
| 996 | + If the master_secret is bytes, it is used as-is. If a str, then we generally expect it to be |
| 997 | + hex. However, this is where we can detect alternatives like "{x,y,z}{pub,priv}key...". These |
| 998 | + are identifiable by their prefix, which is incompatible with hex, so there is no ambiguity. |
| 999 | +
|
953 | 1000 | """ |
954 | | - acct = Account( |
955 | | - crypto = crypto or 'ETH', |
956 | | - format = format, |
957 | | - ).from_seed( |
958 | | - seed = master_secret, |
959 | | - path = path, |
960 | | - ) |
961 | | - log.debug( f"Created {acct} from {len(master_secret)*8}-bit seed, at derivation path {path}" ) |
| 1001 | + if isinstance( master_secret, bytes ) or all( c in string.hexdigits for c in master_secret ): |
| 1002 | + acct = Account( |
| 1003 | + crypto = crypto or 'ETH', |
| 1004 | + format = format, |
| 1005 | + ) |
| 1006 | + acct.from_seed( |
| 1007 | + seed = master_secret, |
| 1008 | + path = path, |
| 1009 | + ) |
| 1010 | + log.debug( f"Created {acct} from {len(master_secret)*8}-bit seed, at derivation path {path}" ) |
| 1011 | + else: |
| 1012 | + # See if we recognize the prefix as a {x,y,z}pub... or .prv... Get the bound function for |
| 1013 | + # initializing the seed. Also, deduce the default format from the x/y/z+pub/prv. |
| 1014 | + default_fmt,from_method = { |
| 1015 | + 'xpub': ('legacy', Account.from_xpubkey), |
| 1016 | + 'xprv': ('legacy', Account.from_xprvkey), |
| 1017 | + 'ypub': ('segwit', Account.from_xpubkey), |
| 1018 | + 'yprv': ('segwit', Account.from_xprvkey), |
| 1019 | + 'zpub': ('bech32', Account.from_xpubkey), |
| 1020 | + 'zprv': ('bech32', Account.from_xprvkey), |
| 1021 | + }.get( master_secret[:4], (None,None) ) |
| 1022 | + if from_method is None: |
| 1023 | + raise ValueError( |
| 1024 | + f"Only x/y/z + pub/prv prefixes supported; {master_secret[:8]+'...'!r} prefix supplied" ) |
| 1025 | + if format is None: |
| 1026 | + format = default_fmt |
| 1027 | + acct = Account( |
| 1028 | + crypto = crypto or 'ETH', |
| 1029 | + format = format |
| 1030 | + ) |
| 1031 | + from_method( acct, master_secret, path ) # It's an unbound method, so pass the instance |
| 1032 | + |
962 | 1033 | return acct |
963 | 1034 |
|
964 | 1035 |
|
|
0 commit comments