URI: 
       tfix #3345: do not require a wallet in order to sweep - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 2ea59aad14cd3c22d13b003d550e86edb7a29c37
   DIR parent 61b69f38568aafcb17db711c92ec80e2eea21443
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Wed, 29 Nov 2017 13:45:02 +0100
       
       fix #3345: do not require a wallet in order to sweep
       
       Diffstat:
         M gui/qt/main_window.py               |       4 ++--
         M lib/commands.py                     |       9 +++++----
         M lib/simple_config.py                |       3 +++
         M lib/wallet.py                       |     161 +++++++++++++++----------------
       
       4 files changed, 89 insertions(+), 88 deletions(-)
       ---
   DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
       t@@ -2364,9 +2364,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                address_e.textChanged.connect(on_address)
                if not d.exec_():
                    return
       -
       +        from electrum.wallet import sweep
                try:
       -            tx = self.wallet.sweep(get_pk(), self.network, self.config, get_address(), None)
       +            tx = sweep(get_pk(), self.network, self.config, get_address(), None)
                except BaseException as e:
                    self.show_message(str(e))
                    return
   DIR diff --git a/lib/commands.py b/lib/commands.py
       t@@ -383,16 +383,17 @@ class Commands:
                    raise BaseException('cannot verify alias', x)
                return out['address']
        
       -    @command('nw')
       +    @command('n')
            def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100):
                """Sweep private keys. Returns a transaction that spends UTXOs from
                privkey to a destination address. The transaction is not
                broadcasted."""
       +        from .wallet import sweep
                tx_fee = satoshis(fee)
                privkeys = privkey.split()
                self.nocheck = nocheck
       -        dest = self._resolver(destination)
       -        tx = self.wallet.sweep(privkeys, self.network, self.config, dest, tx_fee, imax)
       +        #dest = self._resolver(destination)
       +        tx = sweep(privkeys, self.network, self.config, destination, tx_fee, imax)
                return tx.as_dict() if tx else None
        
            @command('wp')
       t@@ -707,7 +708,7 @@ command_options = {
            'nocheck':     (None, "Do not verify aliases"),
            'imax':        (None, "Maximum number of inputs"),
            'fee':         ("-f", "Transaction fee (in BTC)"),
       -    'from_addr':   ("-F", "Source address. If it isn't in the wallet, it will ask for the private key unless supplied in the format public_key:private_key. It's not saved in the wallet."),
       +    'from_addr':   ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."),
            'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"),
            'nbits':       (None, "Number of bits of entropy"),
            'entropy':     (None, "Custom entropy"),
   DIR diff --git a/lib/simple_config.py b/lib/simple_config.py
       t@@ -249,6 +249,9 @@ class SimpleConfig(PrintError):
                    fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2)
                return fee_rate
        
       +    def estimate_fee(self, size):
       +        return int(self.fee_per_kb() * size / 1000.)
       +
            def update_fee_estimates(self, key, value):
                self.fee_estimates[key] = value
                self.fee_estimates_last_updated[key] = time.time()
   DIR diff --git a/lib/wallet.py b/lib/wallet.py
       t@@ -67,6 +67,81 @@ TX_STATUS = [
        ]
        
        
       +
       +def relayfee(network):
       +    RELAY_FEE = 5000
       +    MAX_RELAY_FEE = 50000
       +    f = network.relay_fee if network and network.relay_fee else RELAY_FEE
       +    return min(f, MAX_RELAY_FEE)
       +
       +def dust_threshold(network):
       +    # Change <= dust threshold is added to the tx fee
       +    return 182 * 3 * relayfee(network) / 1000
       +
       +
       +def append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax):
       +    if txin_type != 'p2pk':
       +        address = bitcoin.pubkey_to_address(txin_type, pubkey)
       +        sh = bitcoin.address_to_scripthash(address)
       +    else:
       +        script = bitcoin.public_key_to_p2pk_script(pubkey)
       +        sh = bitcoin.script_to_scripthash(script)
       +        address = '(pubkey)'
       +    u = network.synchronous_get(('blockchain.scripthash.listunspent', [sh]))
       +    for item in u:
       +        if len(inputs) >= imax:
       +            break
       +        item['address'] = address
       +        item['type'] = txin_type
       +        item['prevout_hash'] = item['tx_hash']
       +        item['prevout_n'] = item['tx_pos']
       +        item['pubkeys'] = [pubkey]
       +        item['x_pubkeys'] = [pubkey]
       +        item['signatures'] = [None]
       +        item['num_sig'] = 1
       +        inputs.append(item)
       +
       +def sweep(privkeys, network, config, recipient, fee=None, imax=100):
       +
       +    def find_utxos_for_privkey(txin_type, privkey, compressed):
       +        pubkey = bitcoin.public_key_from_private_key(privkey, compressed)
       +        append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax)
       +        keypairs[pubkey] = privkey, compressed
       +    inputs = []
       +    keypairs = {}
       +    for sec in privkeys:
       +        txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
       +        find_utxos_for_privkey(txin_type, privkey, compressed)
       +        # do other lookups to increase support coverage
       +        if is_minikey(sec):
       +            # minikeys don't have a compressed byte
       +            # we lookup both compressed and uncompressed pubkeys
       +            find_utxos_for_privkey(txin_type, privkey, not compressed)
       +        elif txin_type == 'p2pkh':
       +            # WIF serialization does not distinguish p2pkh and p2pk
       +            # we also search for pay-to-pubkey outputs
       +            find_utxos_for_privkey('p2pk', privkey, compressed)
       +    if not inputs:
       +        raise BaseException(_('No inputs found. (Note that inputs need to be confirmed)'))
       +    total = sum(i.get('value') for i in inputs)
       +    if fee is None:
       +        outputs = [(TYPE_ADDRESS, recipient, total)]
       +        tx = Transaction.from_io(inputs, outputs)
       +        fee = config.estimate_fee(tx.estimated_size())
       +    if total - fee < 0:
       +        raise BaseException(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
       +    if total - fee < dust_threshold(network):
       +        raise BaseException(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network)))
       +
       +    outputs = [(TYPE_ADDRESS, recipient, total - fee)]
       +    locktime = network.get_local_height()
       +
       +    tx = Transaction.from_io(inputs, outputs, locktime=locktime)
       +    tx.set_rbf(True)
       +    tx.sign(keypairs)
       +    return tx
       +
       +
        class Abstract_Wallet(PrintError):
            """
            Wallet classes are created to handle various address generation methods.
       t@@ -89,7 +164,6 @@ class Abstract_Wallet(PrintError):
                self.multiple_change       = storage.get('multiple_change', False)
                self.labels                = storage.get('labels', {})
                self.frozen_addresses      = set(storage.get('frozen_addresses',[]))
       -        self.stored_height         = storage.get('stored_height', 0)       # last known height (for offline mode)
                self.history               = storage.get('addr_history',{})        # address -> list(txid, height)
        
                self.load_keystore()
       t@@ -316,7 +390,7 @@ class Abstract_Wallet(PrintError):
        
            def get_local_height(self):
                """ return last known height if we are offline """
       -        return self.network.get_local_height() if self.network else self.stored_height
       +        return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
        
            def get_tx_height(self, tx_hash):
                """ return the height and timestamp of a verified transaction. """
       t@@ -776,14 +850,10 @@ class Abstract_Wallet(PrintError):
                return status, status_str
        
            def relayfee(self):
       -        RELAY_FEE = 5000
       -        MAX_RELAY_FEE = 50000
       -        f = self.network.relay_fee if self.network and self.network.relay_fee else RELAY_FEE
       -        return min(f, MAX_RELAY_FEE)
       +        return relayfee(self.network)
        
            def dust_threshold(self):
       -        # Change <= dust threshold is added to the tx fee
       -        return 182 * 3 * self.relayfee() / 1000
       +        return dust_threshold(self.network)
        
            def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, change_addr=None):
                # check outputs
       t@@ -826,7 +896,7 @@ class Abstract_Wallet(PrintError):
        
                # Fee estimator
                if fixed_fee is None:
       -            fee_estimator = partial(self.estimate_fee, config)
       +            fee_estimator = self.config.estimate_fee
                else:
                    fee_estimator = lambda size: fixed_fee
        
       t@@ -853,85 +923,12 @@ class Abstract_Wallet(PrintError):
                run_hook('make_unsigned_transaction', self, tx)
                return tx
        
       -    def estimate_fee(self, config, size):
       -        fee = int(config.fee_per_kb() * size / 1000.)
       -        return fee
       -
            def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None):
                coins = self.get_spendable_coins(domain, config)
                tx = self.make_unsigned_transaction(coins, outputs, config, fee, change_addr)
                self.sign_transaction(tx, password)
                return tx
        
       -    def _append_utxos_to_inputs(self, inputs, network, pubkey, txin_type, imax):
       -        if txin_type != 'p2pk':
       -            address = bitcoin.pubkey_to_address(txin_type, pubkey)
       -            sh = bitcoin.address_to_scripthash(address)
       -        else:
       -            script = bitcoin.public_key_to_p2pk_script(pubkey)
       -            sh = bitcoin.script_to_scripthash(script)
       -            address = '(pubkey)'
       -        u = network.synchronous_get(('blockchain.scripthash.listunspent', [sh]))
       -        for item in u:
       -            if len(inputs) >= imax:
       -                break
       -            item['address'] = address
       -            item['type'] = txin_type
       -            item['prevout_hash'] = item['tx_hash']
       -            item['prevout_n'] = item['tx_pos']
       -            item['pubkeys'] = [pubkey]
       -            item['x_pubkeys'] = [pubkey]
       -            item['signatures'] = [None]
       -            item['num_sig'] = 1
       -            inputs.append(item)
       -
       -    def sweep(self, privkeys, network, config, recipient, fee=None, imax=100):
       -
       -        def find_utxos_for_privkey(txin_type, privkey, compressed):
       -            pubkey = bitcoin.public_key_from_private_key(privkey, compressed)
       -            self._append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax)
       -            keypairs[pubkey] = privkey, compressed
       -
       -        inputs = []
       -        keypairs = {}
       -        for sec in privkeys:
       -            txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
       -
       -            find_utxos_for_privkey(txin_type, privkey, compressed)
       -
       -            # do other lookups to increase support coverage
       -            if is_minikey(sec):
       -                # minikeys don't have a compressed byte
       -                # we lookup both compressed and uncompressed pubkeys
       -                find_utxos_for_privkey(txin_type, privkey, not compressed)
       -            elif txin_type == 'p2pkh':
       -                # WIF serialization does not distinguish p2pkh and p2pk
       -                # we also search for pay-to-pubkey outputs
       -                find_utxos_for_privkey('p2pk', privkey, compressed)
       -
       -        if not inputs:
       -            raise BaseException(_('No inputs found. (Note that inputs need to be confirmed)'))
       -
       -        total = sum(i.get('value') for i in inputs)
       -        if fee is None:
       -            outputs = [(TYPE_ADDRESS, recipient, total)]
       -            tx = Transaction.from_io(inputs, outputs)
       -            fee = self.estimate_fee(config, tx.estimated_size())
       -
       -        if total - fee < 0:
       -            raise BaseException(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
       -
       -        if total - fee < self.dust_threshold():
       -            raise BaseException(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, self.dust_threshold()))
       -
       -        outputs = [(TYPE_ADDRESS, recipient, total - fee)]
       -        locktime = self.get_local_height()
       -
       -        tx = Transaction.from_io(inputs, outputs, locktime=locktime)
       -        tx.set_rbf(True)
       -        tx.sign(keypairs)
       -        return tx
       -
            def is_frozen(self, addr):
                return addr in self.frozen_addresses