commit 900a7631cf3588147857688cb5f8efe11c4b2924
parent e1e5167ca9a1b0147c4332ab7c238fd97dadfa4f
Author: SomberNight <somber.night@protonmail.com>
Date: Tue, 31 Mar 2020 05:50:18 +0200
commands: add new cmd "getprivatekeyforpath" to export a WIF for a path
related: #6061
Diffstat:
5 files changed, 37 insertions(+), 6 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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