URI: 
       tMerge branch 'local_tx' - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 0e7e7e3dc5fad0705bf64ead8350c12c1f432d0c
   DIR parent 603345a17223814d4e8ad719b36e49e413abed0e
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Tue, 30 Jan 2018 00:18:44 +0100
       
       Merge branch 'local_tx'
       
       Diffstat:
         M gui/qt/history_list.py              |      40 ++++++++++++++++++++++++++++++-
         M gui/qt/util.py                      |      34 +++++++++++++++++++++++++++++++
         M icons.qrc                           |       1 +
         A icons/offline_tx.png                |       0 
         M lib/commands.py                     |       9 +++++++++
         M lib/synchronizer.py                 |       2 +-
         M lib/wallet.py                       |      76 ++++++++++++++++++-------------
       
       7 files changed, 129 insertions(+), 33 deletions(-)
       ---
   DIR diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
       t@@ -37,6 +37,7 @@ TX_ICONS = [
            "warning.png",
            "unconfirmed.png",
            "unconfirmed.png",
       +    "offline_tx.png",
            "clock1.png",
            "clock2.png",
            "clock3.png",
       t@@ -46,11 +47,12 @@ TX_ICONS = [
        ]
        
        
       -class HistoryList(MyTreeWidget):
       +class HistoryList(MyTreeWidget, AcceptFileDragDrop):
            filter_columns = [2, 3, 4]  # Date, Description, Amount
        
            def __init__(self, parent=None):
                MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
       +        AcceptFileDragDrop.__init__(self, ".txn")
                self.refresh_headers()
                self.setColumnHidden(1, True)
        
       t@@ -158,11 +160,15 @@ class HistoryList(MyTreeWidget):
        
                menu = QMenu()
        
       +        if height == -2:
       +            menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash))
       +
                menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
                if column in self.editable_columns:
                    menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
        
                menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
       +
                if is_unconfirmed and tx:
                    rbf = is_mine and not tx.is_final()
                    if rbf:
       t@@ -176,3 +182,35 @@ class HistoryList(MyTreeWidget):
                if tx_URL:
                    menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL))
                menu.exec_(self.viewport().mapToGlobal(position))
       +
       +    def remove_local_tx(self, delete_tx):
       +        to_delete = {delete_tx}
       +        to_delete |= self.wallet.get_depending_transactions(delete_tx)
       +
       +        question = _("Are you sure you want to remove this transaction?")
       +        if len(to_delete) > 1:
       +            question = _(
       +                "Are you sure you want to remove this transaction and {} child transactions?".format(len(to_delete) - 1)
       +            )
       +
       +        answer = QMessageBox.question(self.parent, _("Please confirm"), question, QMessageBox.Yes, QMessageBox.No)
       +        if answer == QMessageBox.No:
       +            return
       +        for tx in to_delete:
       +            self.wallet.remove_transaction(tx)
       +        self.wallet.save_transactions(write=True)
       +        root = self.invisibleRootItem()
       +        child_count = root.childCount()
       +        _offset = 0
       +        for i in range(child_count):
       +            item = root.child(i - _offset)
       +            if item.data(0, Qt.UserRole) in to_delete:
       +                root.removeChild(item)
       +                _offset += 1
       +
       +    def onFileAdded(self, fn):
       +        with open(fn) as f:
       +            tx = self.parent.tx_from_text(f.read())
       +            self.wallet.add_transaction(tx.txid(), tx)
       +            self.wallet.save_transactions(write=True)
       +            self.on_update()
   DIR diff --git a/gui/qt/util.py b/gui/qt/util.py
       t@@ -635,6 +635,40 @@ class ColorScheme:
                if ColorScheme.has_dark_background(widget):
                    ColorScheme.dark_scheme = True
        
       +
       +class AcceptFileDragDrop:
       +    def __init__(self, file_type=""):
       +        assert isinstance(self, QWidget)
       +        self.setAcceptDrops(True)
       +        self.file_type = file_type
       +
       +    def validateEvent(self, event):
       +        if not event.mimeData().hasUrls():
       +            event.ignore()
       +            return False
       +        for url in event.mimeData().urls():
       +            if not url.toLocalFile().endswith(self.file_type):
       +                event.ignore()
       +                return False
       +        event.accept()
       +        return True
       +
       +    def dragEnterEvent(self, event):
       +        self.validateEvent(event)
       +
       +    def dragMoveEvent(self, event):
       +        if self.validateEvent(event):
       +            event.setDropAction(Qt.CopyAction)
       +
       +    def dropEvent(self, event):
       +        if self.validateEvent(event):
       +            for url in event.mimeData().urls():
       +                self.onFileAdded(url.toLocalFile())
       +
       +    def onFileAdded(self, fn):
       +        raise NotImplementedError()
       +
       +
        if __name__ == "__main__":
            app = QApplication([])
            t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))
   DIR diff --git a/icons.qrc b/icons.qrc
       t@@ -23,6 +23,7 @@
            <file>icons/lock.png</file>
            <file>icons/microphone.png</file>
            <file>icons/network.png</file>
       +    <file>icons/offline_tx.png</file>
            <file>icons/qrcode.png</file>
            <file>icons/qrcode_white.png</file>
            <file>icons/preferences.png</file>
   DIR diff --git a/icons/offline_tx.png b/icons/offline_tx.png
       Binary files differ.
   DIR diff --git a/lib/commands.py b/lib/commands.py
       t@@ -629,6 +629,15 @@ class Commands:
                out = self.wallet.get_payment_request(addr, self.config)
                return self._format_request(out)
        
       +    @command('w')
       +    def addtransaction(self, tx):
       +        """ Add a transaction to the wallet history """
       +        #fixme: we should ensure that tx is related to wallet
       +        tx = Transaction(tx)
       +        self.wallet.add_transaction(tx.txid(), tx)
       +        self.wallet.save_transactions()
       +        return tx.txid()
       +
            @command('wp')
            def signrequest(self, address, password=None):
                "Sign payment request with an OpenAlias"
   DIR diff --git a/lib/synchronizer.py b/lib/synchronizer.py
       t@@ -88,7 +88,7 @@ class Synchronizer(ThreadJob):
                if not params:
                    return
                addr = params[0]
       -        history = self.wallet.get_address_history(addr)
       +        history = self.wallet.history.get(addr, [])
                if self.get_status(history) != result:
                    if self.requested_histories.get(addr) is None:
                        self.requested_histories[addr] = result
   DIR diff --git a/lib/wallet.py b/lib/wallet.py
       t@@ -69,6 +69,7 @@ TX_STATUS = [
            _('Low fee'),
            _('Unconfirmed'),
            _('Not Verified'),
       +    _('Local only'),
        ]
        
        
       t@@ -405,28 +406,30 @@ class Abstract_Wallet(PrintError):
                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. """
       +        """ return the height and timestamp of a transaction. """
                with self.lock:
                    if tx_hash in self.verified_tx:
                        height, timestamp, pos = self.verified_tx[tx_hash]
                        conf = max(self.get_local_height() - height + 1, 0)
                        return height, conf, timestamp
       -            else:
       +            elif tx_hash in self.unverified_tx:
                        height = self.unverified_tx[tx_hash]
                        return height, 0, False
       +            else:
       +                # local transaction
       +                return -2, 0, False
        
            def get_txpos(self, tx_hash):
                "return position, even if the tx is unverified"
                with self.lock:
       -            x = self.verified_tx.get(tx_hash)
       -            y = self.unverified_tx.get(tx_hash)
       -            if x:
       -                height, timestamp, pos = x
       +            if tx_hash in self.verified_tx:
       +                height, timestamp, pos = self.verified_tx[tx_hash]
                        return height, pos
       -            elif y > 0:
       -                return y, 0
       +            elif tx_hash in self.unverified_tx:
       +                height = self.unverified_tx[tx_hash]
       +                return (height, 0) if height>0 else (1e9 - height), 0
                    else:
       -                return 1e12 - y, 0
       +                return (1e9+1, 0)
        
            def is_found(self):
                return self.history.values() != [[]] * len(self.history)
       t@@ -521,7 +524,7 @@ class Abstract_Wallet(PrintError):
                                status = _("%d confirmations") % conf
                            else:
                                status = _('Not verified')
       -                else:
       +                elif height in [-1,0]:
                            status = _('Unconfirmed')
                            if fee is None:
                                fee = self.tx_fees.get(tx_hash)
       t@@ -530,6 +533,9 @@ class Abstract_Wallet(PrintError):
                                fee_per_kb = fee * 1000 / size
                                exp_n = self.network.config.reverse_dynfee(fee_per_kb)
                            can_bump = is_mine and not tx.is_final()
       +                else:
       +                    status = _('Local')
       +                    can_broadcast = self.network is not None
                    else:
                        status = _("Signed")
                        can_broadcast = self.network is not None
       t@@ -551,7 +557,7 @@ class Abstract_Wallet(PrintError):
                return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n
        
            def get_addr_io(self, address):
       -        h = self.history.get(address, [])
       +        h = self.get_address_history(address)
                received = {}
                sent = {}
                for tx_hash, height in h:
       t@@ -650,9 +656,14 @@ class Abstract_Wallet(PrintError):
                    xx += x
                return cc, uu, xx
        
       -    def get_address_history(self, address):
       -        with self.lock:
       -            return self.history.get(address, [])
       +    def get_address_history(self, addr):
       +        h = []
       +        with self.transaction_lock:
       +            for tx_hash in self.transactions:
       +                if addr in self.txi.get(tx_hash, []) or addr in self.txo.get(tx_hash, []):
       +                    tx_height = self.get_tx_height(tx_hash)[0]
       +                    h.append((tx_hash, tx_height))
       +        return h
        
            def find_pay_to_pubkey_address(self, prevout_hash, prevout_n):
                dd = self.txo.get(prevout_hash, {})
       t@@ -749,10 +760,9 @@ class Abstract_Wallet(PrintError):
                    old_hist = self.history.get(addr, [])
                    for tx_hash, height in old_hist:
                        if (tx_hash, height) not in hist:
       -                    # remove tx if it's not referenced in histories
       -                    self.tx_addr_hist[tx_hash].remove(addr)
       -                    if not self.tx_addr_hist[tx_hash]:
       -                        self.remove_transaction(tx_hash)
       +                    # make tx local
       +                    self.unverified_tx.pop(tx_hash, None)
       +                    self.verified_tx.pop(tx_hash, None)
                    self.history[addr] = hist
        
                for tx_hash, tx_height in hist:
       t@@ -845,10 +855,12 @@ class Abstract_Wallet(PrintError):
                        is_lowfee = fee < low_fee * 0.5
                    else:
                        is_lowfee = False
       -            if height==0 and not is_final:
       -                status = 0
       -            elif height < 0:
       +            if height == -2:
       +                status = 5
       +            elif height == -1:
                        status = 1
       +            elif height==0 and not is_final:
       +                status = 0
                    elif height == 0 and is_lowfee:
                        status = 2
                    elif height == 0:
       t@@ -856,9 +868,9 @@ class Abstract_Wallet(PrintError):
                    else:
                        status = 4
                else:
       -            status = 4 + min(conf, 6)
       +            status = 5 + min(conf, 6)
                time_str = format_time(timestamp) if timestamp else _("unknown")
       -        status_str = TX_STATUS[status] if status < 5 else time_str
       +        status_str = TX_STATUS[status] if status < 6 else time_str
                return status, status_str
        
            def relayfee(self):
       t@@ -968,14 +980,6 @@ class Abstract_Wallet(PrintError):
                        # add it in case it was previously unconfirmed
                        self.add_unverified_tx(tx_hash, tx_height)
        
       -        # if we are on a pruning server, remove unverified transactions
       -        with self.lock:
       -            vr = list(self.verified_tx.keys()) + list(self.unverified_tx.keys())
       -        for tx_hash in list(self.transactions):
       -            if tx_hash not in vr:
       -                self.print_error("removing transaction", tx_hash)
       -                self.transactions.pop(tx_hash)
       -
            def start_threads(self, network):
                self.network = network
                if self.network is not None:
       t@@ -1374,6 +1378,16 @@ class Abstract_Wallet(PrintError):
                index = self.get_address_index(addr)
                return self.keystore.decrypt_message(index, message, password)
        
       +    def get_depending_transactions(self, tx_hash):
       +        """Returns all (grand-)children of tx_hash in this wallet."""
       +        children = set()
       +        for other_hash, tx in self.transactions.items():
       +            for input in (tx.inputs()):
       +                if input["prevout_hash"] == tx_hash:
       +                    children.add(other_hash)
       +                    children |= self.get_depending_transactions(other_hash)
       +        return children
       +
        
        class Simple_Wallet(Abstract_Wallet):
            # wallet with a single keystore