detect non-final transactions, and transactions with unconfirmed inputs

This commit is contained in:
ThomasV 2016-05-29 19:53:04 +02:00
parent 2259b741f6
commit 1a46a795a5
12 changed files with 103 additions and 94 deletions

View File

@ -109,10 +109,12 @@ class TxDialog(Factory.Popup):
self.tx_hash = self.tx.hash() self.tx_hash = self.tx.hash()
self.description = self.wallet.get_label(self.tx_hash) self.description = self.wallet.get_label(self.tx_hash)
if self.tx_hash in self.wallet.transactions.keys(): if self.tx_hash in self.wallet.transactions.keys():
conf, timestamp = self.wallet.get_confirmations(self.tx_hash) height, conf, timestamp = self.wallet.get_tx_height(self.tx_hash)
self.status_str = _("%d confirmations")%conf if conf else _('Pending') if conf:
if timestamp: self.status_str = _("%d confirmations")%conf
self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
else:
self.status_str = _('Unconfirmed')
else: else:
self.can_broadcast = self.app.network is not None self.can_broadcast = self.app.network is not None
self.status_str = _('Signed') self.status_str = _('Signed')

View File

@ -118,19 +118,25 @@ class HistoryScreen(CScreen):
def parse_history(self, items): def parse_history(self, items):
for item in items: for item in items:
tx_hash, conf, value, timestamp, balance = item tx_hash, height, conf, timestamp, value, balance = item
time_str = _("unknown") time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] if timestamp else _("unknown")
if conf > 0: if conf == 0:
try: tx = self.app.wallet.transactions.get(tx_hash)
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] is_final = tx.is_final()
except Exception: else:
time_str = _("error") is_final = True
if conf == -1: if not is_final:
time_str = _('Not Verified') time_str = _('Replaceable')
icon = "atlas://gui/kivy/theming/light/close" 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') time_str = _('Unconfirmed')
icon = "atlas://gui/kivy/theming/light/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: elif conf < 6:
conf = max(1, conf) conf = max(1, conf)
icon = "atlas://gui/kivy/theming/light/clock{}".format(conf) icon = "atlas://gui/kivy/theming/light/clock{}".format(conf)

View File

@ -45,15 +45,19 @@ class HistoryList(MyTreeWidget):
run_hook('history_tab_headers', headers) run_hook('history_tab_headers', headers)
self.update_headers(headers) self.update_headers(headers)
def get_icon(self, conf, timestamp): def get_icon(self, height, conf, timestamp, is_final):
time_str = _("unknown") time_str = format_time(timestamp) if timestamp else _("unknown")
if conf > 0: if not is_final:
time_str = format_time(timestamp) time_str = _('Replaceable')
if conf == -1: icon = QIcon(":icons/warning.png")
time_str = _('Not Verified') elif height < 0:
time_str = _('Unconfirmed inputs')
icon = QIcon(":icons/warning.png")
elif height == 0:
time_str = _('Unconfirmed')
icon = QIcon(":icons/unconfirmed.png") icon = QIcon(":icons/unconfirmed.png")
elif conf == 0: elif conf == 0:
time_str = _('Unconfirmed') time_str = _('Not Verified')
icon = QIcon(":icons/unconfirmed.png") icon = QIcon(":icons/unconfirmed.png")
elif conf < 6: elif conf < 6:
icon = QIcon(":icons/clock%d.png"%conf) icon = QIcon(":icons/clock%d.png"%conf)
@ -68,17 +72,18 @@ class HistoryList(MyTreeWidget):
def on_update(self): def on_update(self):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
h = self.wallet.get_history(self.get_domain()) h = self.wallet.get_history(self.get_domain())
item = self.currentItem() item = self.currentItem()
current_tx = item.data(0, Qt.UserRole).toString() if item else None current_tx = item.data(0, Qt.UserRole).toString() if item else None
self.clear() self.clear()
run_hook('history_tab_update_begin') run_hook('history_tab_update_begin')
for h_item in h: for h_item in h:
tx_hash, conf, value, timestamp, balance = h_item tx_hash, height, conf, timestamp, value, balance = h_item
if conf is None and timestamp is None: if conf == 0:
continue # skip history in offline mode tx = self.wallet.transactions.get(tx_hash)
is_final = tx.is_final()
icon, time_str = self.get_icon(conf, timestamp) 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) v_str = self.parent.format_amount(value, True, whitespaces=True)
balance_str = self.parent.format_amount(balance, whitespaces=True) balance_str = self.parent.format_amount(balance, whitespaces=True)
label = self.wallet.get_label(tx_hash) label = self.wallet.get_label(tx_hash)
@ -100,8 +105,8 @@ class HistoryList(MyTreeWidget):
if current_tx == tx_hash: if current_tx == tx_hash:
self.setCurrentItem(item) self.setCurrentItem(item)
def update_item(self, tx_hash, conf, timestamp): def update_item(self, tx_hash, height, conf, timestamp):
icon, time_str = self.get_icon(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) items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1)
if items: if items:
item = items[0] item = items[0]
@ -125,10 +130,10 @@ class HistoryList(MyTreeWidget):
column_data = item.text(column) column_data = item.text(column)
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) 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) tx = self.wallet.transactions.get(tx_hash)
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) 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 = QMenu()
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))

View File

@ -2196,14 +2196,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
history = wallet.get_history() history = wallet.get_history()
lines = [] lines = []
for item in history: for item in history:
tx_hash, confirmations, value, timestamp, balance = item tx_hash, height, conf, timestamp, value, balance = item
if confirmations: if height>0:
if timestamp is not None: if timestamp is not None:
time_string = format_time(timestamp) time_string = format_time(timestamp)
else: else:
time_string = "unknown" time_string = _("unverified")
else: else:
time_string = "unconfirmed" time_string = _("unconfirmed")
if value is not None: if value is not None:
value_string = format_satoshis(value, True) value_string = format_satoshis(value, True)

View File

@ -183,17 +183,19 @@ class TxDialog(QDialog, MessageBoxMixin):
self.broadcast_button.hide() self.broadcast_button.hide()
if self.tx.is_complete(): if self.tx.is_complete():
status = _("Signed")
if tx_hash in self.wallet.transactions.keys(): if tx_hash in self.wallet.transactions.keys():
desc = self.wallet.get_label(tx_hash) desc = self.wallet.get_label(tx_hash)
conf, timestamp = self.wallet.get_confirmations(tx_hash) height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
if timestamp: if height > 0:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] if conf:
status = _("%d confirmations") % height
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
else:
status = _('Not verified')
else: else:
time_str = _('Pending') status = _('Unconfirmed')
status = _("%d confirmations")%conf
else: else:
status = _("Signed")
self.broadcast_button.show() self.broadcast_button.show()
# cannot broadcast when offline # cannot broadcast when offline
if self.main_window.network is None: if self.main_window.network is None:

View File

@ -106,9 +106,8 @@ class ElectrumGui:
b = 0 b = 0
self.history = [] self.history = []
for item in self.wallet.get_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: if conf:
try: try:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]

View File

@ -33,6 +33,7 @@
<file>icons/unconfirmed.png</file> <file>icons/unconfirmed.png</file>
<file>icons/unpaid.png</file> <file>icons/unpaid.png</file>
<file>icons/unlock.png</file> <file>icons/unlock.png</file>
<file>icons/warning.png</file>
<file>icons/zoom.png</file> <file>icons/zoom.png</file>
</qresource> </qresource>
</RCC> </RCC>

BIN
icons/warning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -450,20 +450,21 @@ class Commands:
balance = 0 balance = 0
out = [] out = []
for item in self.wallet.get_history(): for item in self.wallet.get_history():
tx_hash, conf, value, timestamp, balance = item tx_hash, height, conf, timestamp, value, balance = item
try: if timestamp:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] date = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
except Exception: else:
time_str = "----" date = "----"
label = self.wallet.get_label(tx_hash) label = self.wallet.get_label(tx_hash)
out.append({ out.append({
'txid':tx_hash, 'txid': tx_hash,
'timestamp':timestamp, 'timestamp': timestamp,
'date':"%16s"%time_str, 'date': date,
'label':label, 'label': label,
'value':float(value)/COIN if value is not None else None, 'value': float(value)/COIN if value is not None else None,
'confirmations':conf} 'height': height,
) 'confirmations': conf
})
return out return out
@command('w') @command('w')

View File

@ -43,7 +43,7 @@ class SPV(ThreadJob):
unverified = self.wallet.get_unverified_txs() unverified = self.wallet.get_unverified_txs()
for tx_hash, tx_height in unverified.items(): for tx_hash, tx_height in unverified.items():
# do not request merkle branch before headers are available # 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', request = ('blockchain.transaction.get_merkle',
[tx_hash, tx_height]) [tx_hash, tx_height])
self.network.send([request], self.verify_merkle) self.network.send([request], self.verify_merkle)

View File

@ -35,7 +35,7 @@ import re
import stat import stat
from functools import partial from functools import partial
from unicodedata import normalize from unicodedata import normalize
from collections import namedtuple from collections import namedtuple, defaultdict
from i18n import _ from i18n import _
from util import NotEnoughFunds, PrintError, profiler from util import NotEnoughFunds, PrintError, profiler
@ -193,7 +193,8 @@ class Abstract_Wallet(PrintError):
# Transactions pending verification. A map from tx hash to transaction # Transactions pending verification. A map from tx hash to transaction
# height. Access is not contended so no lock is needed. # 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. # Verified transactions. Each value is a (height, timestamp, block_pos) tuple. Access with self.lock.
self.verified_tx = storage.get('verified_tx3',{}) self.verified_tx = storage.get('verified_tx3',{})
@ -455,8 +456,8 @@ class Abstract_Wallet(PrintError):
return decrypted return decrypted
def add_unverified_tx(self, tx_hash, tx_height): def add_unverified_tx(self, tx_hash, tx_height):
# Only add if confirmed and not verified # tx will be verified only if height > 0
if tx_height > 0 and tx_hash not in self.verified_tx: if tx_hash not in self.verified_tx:
self.unverified_tx[tx_hash] = tx_height self.unverified_tx[tx_hash] = tx_height
def add_verified_tx(self, tx_hash, info): def add_verified_tx(self, tx_hash, info):
@ -465,9 +466,8 @@ class Abstract_Wallet(PrintError):
with self.lock: with self.lock:
self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos) self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos)
self.storage.put('verified_tx3', self.verified_tx) self.storage.put('verified_tx3', self.verified_tx)
height, conf, timestamp = self.get_tx_height(tx_hash)
conf, timestamp = self.get_confirmations(tx_hash) self.network.trigger_callback('verified', tx_hash, height, conf, timestamp)
self.network.trigger_callback('verified', tx_hash, conf, timestamp)
def get_unverified_txs(self): def get_unverified_txs(self):
'''Returns a map from tx hash to transaction height''' '''Returns a map from tx hash to transaction height'''
@ -488,34 +488,29 @@ class Abstract_Wallet(PrintError):
""" return last known height if we are offline """ """ 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.stored_height
def get_confirmations(self, tx): def get_tx_height(self, tx_hash):
""" return the number of confirmations of a monitored transaction. """ """ return the height and timestamp of a verified transaction. """
with self.lock: with self.lock:
if tx in self.verified_tx: if tx_hash in self.verified_tx:
height, timestamp, pos = self.verified_tx[tx] height, timestamp, pos = self.verified_tx[tx_hash]
conf = (self.get_local_height() - height + 1) conf = max(self.get_local_height() - height + 1, 0)
if conf <= 0: timestamp = None return height, conf, timestamp
elif tx in self.unverified_tx:
conf = -1
timestamp = None
else: else:
conf = 0 height = self.unverified_tx[tx_hash]
timestamp = None return height, 0, False
return conf, timestamp
def get_txpos(self, tx_hash): def get_txpos(self, tx_hash):
"return position, even if the tx is unverified" "return position, even if the tx is unverified"
with self.lock: with self.lock:
x = self.verified_tx.get(tx_hash) x = self.verified_tx.get(tx_hash)
y = self.unverified_tx.get(tx_hash) y = self.unverified_tx.get(tx_hash)
if x: if x:
height, timestamp, pos = x height, timestamp, pos = x
return height, pos return height, pos
elif y: elif y > 0:
return y, 0 return y, 0
else: else:
return 1e12, 0 return 1e12, 0
def is_found(self): def is_found(self):
return self.history.values() != [[]] * len(self.history) return self.history.values() != [[]] * len(self.history)
@ -827,7 +822,6 @@ class Abstract_Wallet(PrintError):
self.tx_addr_hist[tx_hash].remove(addr) self.tx_addr_hist[tx_hash].remove(addr)
if not self.tx_addr_hist[tx_hash]: if not self.tx_addr_hist[tx_hash]:
self.remove_transaction(tx_hash) self.remove_transaction(tx_hash)
self.history[addr] = hist self.history[addr] = hist
for tx_hash, tx_height in hist: for tx_hash, tx_height in hist:
@ -846,7 +840,6 @@ class Abstract_Wallet(PrintError):
self.save_transactions() self.save_transactions()
def get_history(self, domain=None): def get_history(self, domain=None):
from collections import defaultdict
# get domain # get domain
if domain is None: if domain is None:
domain = self.get_account_addresses(None) domain = self.get_account_addresses(None)
@ -865,9 +858,10 @@ class Abstract_Wallet(PrintError):
# 2. create sorted history # 2. create sorted history
history = [] history = []
for tx_hash, delta in tx_deltas.items(): for tx_hash in tx_deltas:
conf, timestamp = self.get_confirmations(tx_hash) delta = tx_deltas[tx_hash]
history.append((tx_hash, conf, delta, timestamp)) 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.sort(key = lambda x: self.get_txpos(x[0]))
history.reverse() history.reverse()
@ -875,9 +869,8 @@ class Abstract_Wallet(PrintError):
c, u, x = self.get_balance(domain) c, u, x = self.get_balance(domain)
balance = c + u + x balance = c + u + x
h2 = [] h2 = []
for item in history: for tx_hash, height, conf, timestamp, delta in history:
tx_hash, conf, delta, timestamp = item h2.append((tx_hash, height, conf, timestamp, delta, balance))
h2.append((tx_hash, conf, delta, timestamp, balance))
if balance is None or delta is None: if balance is None or delta is None:
balance = None balance = None
else: else:
@ -1076,7 +1069,7 @@ class Abstract_Wallet(PrintError):
for addr, hist in self.history.items(): for addr, hist in self.history.items():
for tx_hash, tx_height in hist: for tx_hash, tx_height in hist:
# add it in case it was previously unconfirmed # 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 # if we are on a pruning server, remove unverified transactions
with self.lock: with self.lock:

View File

@ -217,7 +217,7 @@ class Plugin(FxPlugin, QObject):
def history_tab_update(self, tx, entry): def history_tab_update(self, tx, entry):
if not self.show_history(): if not self.show_history():
return return
tx_hash, conf, value, timestamp, balance = tx tx_hash, height, conf, timestamp, value, balance = tx
if conf <= 0: if conf <= 0:
date = timestamp_to_datetime(time.time()) date = timestamp_to_datetime(time.time())
else: else: