diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index a8634b189..e0575d606 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -449,7 +449,7 @@ class AddressSynchronizer(Logger): domain = set(domain) # 1. Get the history of each address in the domain, maintain the # delta of a tx as the sum of its deltas on domain addresses - tx_deltas = defaultdict(int) + tx_deltas = defaultdict(int) # type: Dict[str, Optional[int]] for addr in domain: h = self.get_address_history(addr) for tx_hash, height in h: diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 6316dcef1..16d2fda9c 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -565,8 +565,8 @@ def is_segwit_script_type(txin_type: str) -> bool: return txin_type in ('p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh') -def serialize_privkey(secret: bytes, compressed: bool, txin_type: str, - internal_use: bool=False) -> str: +def serialize_privkey(secret: bytes, compressed: bool, txin_type: str, *, + internal_use: bool = False) -> str: # we only export secrets inside curve range secret = ecc.ECPrivkey.normalize_secret_bytes(secret) if internal_use: diff --git a/electrum/commands.py b/electrum/commands.py index d2b836296..39b25f470 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -414,6 +414,13 @@ class Commands: domain = address return [wallet.export_private_key(address, password) for address in domain] + @command('wp') + async def getprivatekeyforpath(self, path, password=None, wallet: Abstract_Wallet = None): + """Get private key corresponding to derivation path (address index). + 'path' can be either a str such as "m/0/50", or a list of ints such as [0, 50]. + """ + return wallet.export_private_key_for_path(path, password) + @command('w') async def ismine(self, address, wallet: Abstract_Wallet = None): """Check if address is in wallet. Return true if and only address is in wallet""" diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py index f57b4a579..9f4330afb 100644 --- a/electrum/tests/test_commands.py +++ b/electrum/tests/test_commands.py @@ -180,3 +180,17 @@ class TestCommandsTestnet(TestCaseForTestnet): } self.assertEqual("0200000000010139c5375fe9da7bd377c1783002b129f8c57d3e724d62f5eacb9739ca691a229d0100000000feffffff01301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb0247304402206367fb2ddd723985f5f51e0f2435084c0a66f5c26f4403a75d3dd417b71a20450220545dc3637bcb49beedbbdf5063e05cad63be91af4f839886451c30ecd6edf1d20121021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da00000000", cmds._run('serialize', (jsontx,))) + + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_getprivatekeyforpath(self, mock_save_db): + wallet = restore_wallet_from_text('north rent dawn bunker hamster invest wagon market romance pig either squeeze', + gap_limit=2, + path='if_this_exists_mocking_failed_648151893', + config=self.config)['wallet'] + cmds = Commands(config=self.config) + self.assertEqual("p2wpkh:cUzm7zPpWgLYeURgff4EsoMjhskCpsviBH4Y3aZcrBX8UJSRPjC2", + cmds._run('getprivatekeyforpath', ([0, 10000],), wallet=wallet)) + self.assertEqual("p2wpkh:cUzm7zPpWgLYeURgff4EsoMjhskCpsviBH4Y3aZcrBX8UJSRPjC2", + cmds._run('getprivatekeyforpath', ("m/0/10000",), wallet=wallet)) + self.assertEqual("p2wpkh:cQAj4WGf1socCPCJNMjXYCJ8Bs5JUAk5pbDr4ris44QdgAXcV24S", + cmds._run('getprivatekeyforpath', ("m/5h/100000/88h/7",), wallet=wallet)) diff --git a/electrum/wallet.py b/electrum/wallet.py index 497ce3d42..9a5d71d30 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -44,7 +44,7 @@ from abc import ABC, abstractmethod import itertools from .i18n import _ -from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath +from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_path_to_list_of_uint32 from .crypto import sha256 from .util import (NotEnoughFunds, UserCancelled, profiler, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, @@ -462,7 +462,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): """Return script type of wallet address.""" pass - def export_private_key(self, address, password) -> str: + def export_private_key(self, address: str, password: Optional[str]) -> str: if self.is_watching_only(): raise Exception(_("This is a watching-only wallet")) if not is_address(address): @@ -475,6 +475,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC): serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type) return serialized_privkey + def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str: + raise Exception("this wallet is not deterministic") + @abstractmethod def get_public_keys(self, address: str) -> Sequence[str]: pass @@ -2201,6 +2204,13 @@ class Deterministic_Wallet(Abstract_Wallet): pubkeys = self.derive_pubkeys(for_change, n) return self.pubkeys_to_address(pubkeys) + def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str: + if isinstance(path, str): + path = convert_bip32_path_to_list_of_uint32(path) + pk, compressed = self.keystore.get_private_key(path, password) + txin_type = self.get_txin_type() # assumes no mixed-scripts in wallet + return bitcoin.serialize_privkey(pk, compressed, txin_type) + def get_public_keys_with_deriv_info(self, address: str): der_suffix = self.get_address_index(address) der_suffix = [int(x) for x in der_suffix] @@ -2301,7 +2311,7 @@ class Deterministic_Wallet(Abstract_Wallet): def get_fingerprint(self): return self.get_master_public_key() - def get_txin_type(self, address): + def get_txin_type(self, address=None): return self.txin_type