URI: 
       tdetect non-final transactions, and transactions with unconfirmed inputs - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 1a46a795a5cf510789f21c47771363a7b31f1b4a
   DIR parent 2259b741f6d3531837874139a19b4a8cdd8c0d8f
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Sun, 29 May 2016 19:53:04 +0200
       
       detect non-final transactions, and transactions with unconfirmed inputs
       
       Diffstat:
         M gui/kivy/uix/dialogs/tx_dialog.py   |       8 +++++---
         M gui/kivy/uix/screens.py             |      26 ++++++++++++++++----------
         M gui/qt/history_list.py              |      39 +++++++++++++++++--------------
         M gui/qt/main_window.py               |       8 ++++----
         M gui/qt/transaction_dialog.py        |      16 +++++++++-------
         M gui/text.py                         |       3 +--
         M icons.qrc                           |       1 +
         A icons/warning.png                   |       0 
         M lib/commands.py                     |      25 +++++++++++++------------
         M lib/verifier.py                     |       2 +-
         M lib/wallet.py                       |      67 ++++++++++++++-----------------
         M plugins/exchange_rate/qt.py         |       2 +-
       
       12 files changed, 103 insertions(+), 94 deletions(-)
       ---
   DIR diff --git a/gui/kivy/uix/dialogs/tx_dialog.py b/gui/kivy/uix/dialogs/tx_dialog.py
       t@@ -109,10 +109,12 @@ class TxDialog(Factory.Popup):
                    self.tx_hash = self.tx.hash()
                    self.description = self.wallet.get_label(self.tx_hash)
                    if self.tx_hash in self.wallet.transactions.keys():
       -                conf, timestamp = self.wallet.get_confirmations(self.tx_hash)
       -                self.status_str = _("%d confirmations")%conf if conf else _('Pending')
       -                if timestamp:
       +                height, conf, timestamp = self.wallet.get_tx_height(self.tx_hash)
       +                if conf:
       +                    self.status_str = _("%d confirmations")%conf
                            self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
       +                else:
       +                    self.status_str =  _('Unconfirmed')
                    else:
                        self.can_broadcast = self.app.network is not None
                        self.status_str = _('Signed')
   DIR diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py
       t@@ -118,19 +118,25 @@ class HistoryScreen(CScreen):
        
            def parse_history(self, items):
                for item in items:
       -            tx_hash, conf, value, timestamp, balance = item
       -            time_str = _("unknown")
       -            if conf > 0:
       -                try:
       -                    time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
       -                except Exception:
       -                    time_str = _("error")
       -            if conf == -1:
       -                time_str = _('Not Verified')
       +            tx_hash, height, conf, timestamp, value, balance = item
       +            time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] if timestamp else _("unknown")
       +            if conf == 0:
       +                tx = self.app.wallet.transactions.get(tx_hash)
       +                is_final = tx.is_final()
       +            else:
       +                is_final = True
       +            if not is_final:
       +                time_str = _('Replaceable')
                        icon = "atlas://gui/kivy/theming/light/close"
       -            elif conf == 0:
       +            elif height < 0:
       +                time_str = _('Unconfirmed inputs')
       +                icon = "atlas://gui/kivy/theming/light/close"
       +            elif height == 0:
                        time_str = _('Unconfirmed')
                        icon = "atlas://gui/kivy/theming/light/unconfirmed"
       +            elif conf == 0:
       +                time_str = _('Not Verified')
       +                icon = "atlas://gui/kivy/theming/light/close"
                    elif conf < 6:
                        conf = max(1, conf)
                        icon = "atlas://gui/kivy/theming/light/clock{}".format(conf)
   DIR diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
       t@@ -45,15 +45,19 @@ class HistoryList(MyTreeWidget):
                run_hook('history_tab_headers', headers)
                self.update_headers(headers)
        
       -    def get_icon(self, conf, timestamp):
       -        time_str = _("unknown")
       -        if conf > 0:
       -            time_str = format_time(timestamp)
       -        if conf == -1:
       -            time_str = _('Not Verified')
       +    def get_icon(self, height, conf, timestamp, is_final):
       +        time_str = format_time(timestamp) if timestamp else _("unknown")
       +        if not is_final:
       +            time_str = _('Replaceable')
       +            icon = QIcon(":icons/warning.png")
       +        elif height < 0:
       +            time_str = _('Unconfirmed inputs')
       +            icon = QIcon(":icons/warning.png")
       +        elif height == 0:
       +            time_str = _('Unconfirmed')
                    icon = QIcon(":icons/unconfirmed.png")
                elif conf == 0:
       -            time_str = _('Unconfirmed')
       +            time_str = _('Not Verified')
                    icon = QIcon(":icons/unconfirmed.png")
                elif conf < 6:
                    icon = QIcon(":icons/clock%d.png"%conf)
       t@@ -68,17 +72,18 @@ class HistoryList(MyTreeWidget):
            def on_update(self):
                self.wallet = self.parent.wallet
                h = self.wallet.get_history(self.get_domain())
       -
                item = self.currentItem()
                current_tx = item.data(0, Qt.UserRole).toString() if item else None
                self.clear()
                run_hook('history_tab_update_begin')
                for h_item in h:
       -            tx_hash, conf, value, timestamp, balance = h_item
       -            if conf is None and timestamp is None:
       -                continue  # skip history in offline mode
       -
       -            icon, time_str = self.get_icon(conf, timestamp)
       +            tx_hash, height, conf, timestamp, value, balance = h_item
       +            if conf == 0:
       +                tx = self.wallet.transactions.get(tx_hash)
       +                is_final = tx.is_final()
       +            else:
       +                is_final = True
       +            icon, time_str = self.get_icon(height, conf, timestamp, is_final)
                    v_str = self.parent.format_amount(value, True, whitespaces=True)
                    balance_str = self.parent.format_amount(balance, whitespaces=True)
                    label = self.wallet.get_label(tx_hash)
       t@@ -100,8 +105,8 @@ class HistoryList(MyTreeWidget):
                    if current_tx == tx_hash:
                        self.setCurrentItem(item)
        
       -    def update_item(self, tx_hash, conf, timestamp):
       -        icon, time_str = self.get_icon(conf, timestamp)
       +    def update_item(self, tx_hash, height, conf, timestamp):
       +        icon, time_str = self.get_icon(height, conf, timestamp, True)
                items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1)
                if items:
                    item = items[0]
       t@@ -125,10 +130,10 @@ class HistoryList(MyTreeWidget):
                    column_data = item.text(column)
        
                tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
       -        conf, timestamp = self.wallet.get_confirmations(tx_hash)
       +        height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
                tx = self.wallet.transactions.get(tx_hash)
                is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
       -        rbf = is_mine and (conf == 0) and tx and not tx.is_final()
       +        rbf = is_mine and height <=0 and tx and not tx.is_final()
                menu = QMenu()
        
                menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
   DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
       t@@ -2196,14 +2196,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                history = wallet.get_history()
                lines = []
                for item in history:
       -            tx_hash, confirmations, value, timestamp, balance = item
       -            if confirmations:
       +            tx_hash, height, conf, timestamp, value, balance = item
       +            if height>0:
                        if timestamp is not None:
                            time_string = format_time(timestamp)
                        else:
       -                    time_string = "unknown"
       +                    time_string = _("unverified")
                    else:
       -                time_string = "unconfirmed"
       +                time_string = _("unconfirmed")
        
                    if value is not None:
                        value_string = format_satoshis(value, True)
   DIR diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py
       t@@ -183,17 +183,19 @@ class TxDialog(QDialog, MessageBoxMixin):
                self.broadcast_button.hide()
        
                if self.tx.is_complete():
       -            status = _("Signed")
       -
                    if tx_hash in self.wallet.transactions.keys():
                        desc = self.wallet.get_label(tx_hash)
       -                conf, timestamp = self.wallet.get_confirmations(tx_hash)
       -                if timestamp:
       -                    time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
       +                height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
       +                if height > 0:
       +                    if conf:
       +                        status = _("%d confirmations") % height
       +                        time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
       +                    else:
       +                        status = _('Not verified')
                        else:
       -                    time_str = _('Pending')
       -                status = _("%d confirmations")%conf
       +                    status = _('Unconfirmed')
                    else:
       +                status = _("Signed")
                        self.broadcast_button.show()
                        # cannot broadcast when offline
                        if self.main_window.network is None:
   DIR diff --git a/gui/text.py b/gui/text.py
       t@@ -106,9 +106,8 @@ class ElectrumGui:
        
                b = 0
                self.history = []
       -
                for item in self.wallet.get_history():
       -            tx_hash, conf, value, timestamp, balance = item
       +            tx_hash, height, conf, timestamp, value, balance = item
                    if conf:
                        try:
                            time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
   DIR diff --git a/icons.qrc b/icons.qrc
       t@@ -33,6 +33,7 @@
            <file>icons/unconfirmed.png</file>
            <file>icons/unpaid.png</file>
            <file>icons/unlock.png</file>
       +    <file>icons/warning.png</file>
            <file>icons/zoom.png</file>
          </qresource>
        </RCC>
   DIR diff --git a/icons/warning.png b/icons/warning.png
       Binary files differ.
   DIR diff --git a/lib/commands.py b/lib/commands.py
       t@@ -450,20 +450,21 @@ class Commands:
                balance = 0
                out = []
                for item in self.wallet.get_history():
       -            tx_hash, conf, value, timestamp, balance = item
       -            try:
       -                time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
       -            except Exception:
       -                time_str = "----"
       +            tx_hash, height, conf, timestamp, value, balance = item
       +            if timestamp:
       +                date = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
       +            else:
       +                date = "----"
                    label = self.wallet.get_label(tx_hash)
                    out.append({
       -                'txid':tx_hash,
       -                'timestamp':timestamp,
       -                'date':"%16s"%time_str,
       -                'label':label,
       -                'value':float(value)/COIN if value is not None else None,
       -                'confirmations':conf}
       -            )
       +                'txid': tx_hash,
       +                'timestamp': timestamp,
       +                'date': date,
       +                'label': label,
       +                'value': float(value)/COIN if value is not None else None,
       +                'height': height,
       +                'confirmations': conf
       +            })
                return out
        
            @command('w')
   DIR diff --git a/lib/verifier.py b/lib/verifier.py
       t@@ -43,7 +43,7 @@ class SPV(ThreadJob):
                unverified = self.wallet.get_unverified_txs()
                for tx_hash, tx_height in unverified.items():
                    # do not request merkle branch before headers are available
       -            if tx_hash not in self.merkle_roots and tx_height <= lh:
       +            if tx_height>0 and tx_hash not in self.merkle_roots and tx_height <= lh:
                        request = ('blockchain.transaction.get_merkle',
                                   [tx_hash, tx_height])
                        self.network.send([request], self.verify_merkle)
   DIR diff --git a/lib/wallet.py b/lib/wallet.py
       t@@ -35,7 +35,7 @@ import re
        import stat
        from functools import partial
        from unicodedata import normalize
       -from collections import namedtuple
       +from collections import namedtuple, defaultdict
        
        from i18n import _
        from util import NotEnoughFunds, PrintError, profiler
       t@@ -193,7 +193,8 @@ class Abstract_Wallet(PrintError):
        
                # Transactions pending verification.  A map from tx hash to transaction
                # height.  Access is not contended so no lock is needed.
       -        self.unverified_tx = {}
       +        self.unverified_tx = defaultdict(int)
       +
                # Verified transactions.  Each value is a (height, timestamp, block_pos) tuple.  Access with self.lock.
                self.verified_tx   = storage.get('verified_tx3',{})
        
       t@@ -455,8 +456,8 @@ class Abstract_Wallet(PrintError):
                return decrypted
        
            def add_unverified_tx(self, tx_hash, tx_height):
       -        # Only add if confirmed and not verified
       -        if tx_height > 0 and tx_hash not in self.verified_tx:
       +        # tx will be verified only if height > 0
       +        if tx_hash not in self.verified_tx:
                    self.unverified_tx[tx_hash] = tx_height
        
            def add_verified_tx(self, tx_hash, info):
       t@@ -465,9 +466,8 @@ class Abstract_Wallet(PrintError):
                with self.lock:
                    self.verified_tx[tx_hash] = info  # (tx_height, timestamp, pos)
                self.storage.put('verified_tx3', self.verified_tx)
       -
       -        conf, timestamp = self.get_confirmations(tx_hash)
       -        self.network.trigger_callback('verified', tx_hash, conf, timestamp)
       +        height, conf, timestamp = self.get_tx_height(tx_hash)
       +        self.network.trigger_callback('verified', tx_hash, height, conf, timestamp)
        
            def get_unverified_txs(self):
                '''Returns a map from tx hash to transaction height'''
       t@@ -488,34 +488,29 @@ class Abstract_Wallet(PrintError):
                """ return last known height if we are offline """
                return self.network.get_local_height() if self.network else self.stored_height
        
       -    def get_confirmations(self, tx):
       -        """ return the number of confirmations of a monitored transaction. """
       +    def get_tx_height(self, tx_hash):
       +        """ return the height and timestamp of a verified transaction. """
                with self.lock:
       -            if tx in self.verified_tx:
       -                height, timestamp, pos = self.verified_tx[tx]
       -                conf = (self.get_local_height() - height + 1)
       -                if conf <= 0: timestamp = None
       -            elif tx in self.unverified_tx:
       -                conf = -1
       -                timestamp = None
       +            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:
       -                conf = 0
       -                timestamp = None
       -
       -        return conf, timestamp
       +                height = self.unverified_tx[tx_hash]
       +                return height, 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
       -            return height, pos
       -        elif y:
       -            return y, 0
       -        else:
       -            return 1e12, 0
       +            y = self.unverified_tx.get(tx_hash)
       +            if x:
       +                height, timestamp, pos = x
       +                return height, pos
       +            elif y > 0:
       +                return y, 0
       +            else:
       +                return 1e12, 0
        
            def is_found(self):
                return self.history.values() != [[]] * len(self.history)
       t@@ -827,7 +822,6 @@ class Abstract_Wallet(PrintError):
                            self.tx_addr_hist[tx_hash].remove(addr)
                            if not self.tx_addr_hist[tx_hash]:
                                self.remove_transaction(tx_hash)
       -
                    self.history[addr] = hist
        
                for tx_hash, tx_height in hist:
       t@@ -846,7 +840,6 @@ class Abstract_Wallet(PrintError):
                self.save_transactions()
        
            def get_history(self, domain=None):
       -        from collections import defaultdict
                # get domain
                if domain is None:
                    domain = self.get_account_addresses(None)
       t@@ -865,9 +858,10 @@ class Abstract_Wallet(PrintError):
        
                # 2. create sorted history
                history = []
       -        for tx_hash, delta in tx_deltas.items():
       -            conf, timestamp = self.get_confirmations(tx_hash)
       -            history.append((tx_hash, conf, delta, timestamp))
       +        for tx_hash in tx_deltas:
       +            delta = tx_deltas[tx_hash]
       +            height, conf, timestamp = self.get_tx_height(tx_hash)
       +            history.append((tx_hash, height, conf, timestamp, delta))
                history.sort(key = lambda x: self.get_txpos(x[0]))
                history.reverse()
        
       t@@ -875,9 +869,8 @@ class Abstract_Wallet(PrintError):
                c, u, x = self.get_balance(domain)
                balance = c + u + x
                h2 = []
       -        for item in history:
       -            tx_hash, conf, delta, timestamp = item
       -            h2.append((tx_hash, conf, delta, timestamp, balance))
       +        for tx_hash, height, conf, timestamp, delta in history:
       +            h2.append((tx_hash, height, conf, timestamp, delta, balance))
                    if balance is None or delta is None:
                        balance = None
                    else:
       t@@ -1076,7 +1069,7 @@ class Abstract_Wallet(PrintError):
                for addr, hist in self.history.items():
                    for tx_hash, tx_height in hist:
                        # add it in case it was previously unconfirmed
       -                self.add_unverified_tx (tx_hash, tx_height)
       +                self.add_unverified_tx(tx_hash, tx_height)
        
                # if we are on a pruning server, remove unverified transactions
                with self.lock:
   DIR diff --git a/plugins/exchange_rate/qt.py b/plugins/exchange_rate/qt.py
       t@@ -217,7 +217,7 @@ class Plugin(FxPlugin, QObject):
            def history_tab_update(self, tx, entry):
                if not self.show_history():
                    return
       -        tx_hash, conf, value, timestamp, balance = tx
       +        tx_hash, height, conf, timestamp, value, balance = tx
                if conf <= 0:
                    date = timestamp_to_datetime(time.time())
                else: