Skip to content

Commit

Permalink
detect non-final transactions, and transactions with unconfirmed inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
ecdsa committed May 29, 2016
1 parent 2259b74 commit 1a46a79
Show file tree
Hide file tree
Showing 12 changed files with 103 additions and 94 deletions.
8 changes: 5 additions & 3 deletions gui/kivy/uix/dialogs/tx_dialog.py
Expand Up @@ -109,10 +109,12 @@ def update(self):
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')
Expand Down
26 changes: 16 additions & 10 deletions gui/kivy/uix/screens.py
Expand Up @@ -118,19 +118,25 @@ def callback(text):

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)
Expand Down
39 changes: 22 additions & 17 deletions gui/qt/history_list.py
Expand Up @@ -45,15 +45,19 @@ def refresh_headers(self):
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)
Expand All @@ -68,17 +72,18 @@ def get_domain(self):
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)
Expand All @@ -100,8 +105,8 @@ def on_update(self):
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]
Expand All @@ -125,10 +130,10 @@ def create_menu(self, position):
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))
Expand Down
8 changes: 4 additions & 4 deletions gui/qt/main_window.py
Expand Up @@ -2196,14 +2196,14 @@ def do_export_history(self, wallet, fileName, is_csv):
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)
Expand Down
16 changes: 9 additions & 7 deletions gui/qt/transaction_dialog.py
Expand Up @@ -183,17 +183,19 @@ def update(self):
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:
Expand Down
3 changes: 1 addition & 2 deletions gui/text.py
Expand Up @@ -106,9 +106,8 @@ def update_history(self):

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]
Expand Down
1 change: 1 addition & 0 deletions icons.qrc
Expand Up @@ -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>
Binary file added icons/warning.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 13 additions & 12 deletions lib/commands.py
Expand Up @@ -450,20 +450,21 @@ def history(self):
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')
Expand Down
2 changes: 1 addition & 1 deletion lib/verifier.py
Expand Up @@ -43,7 +43,7 @@ def run(self):
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)
Expand Down
67 changes: 30 additions & 37 deletions lib/wallet.py
Expand Up @@ -35,7 +35,7 @@
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
Expand Down Expand Up @@ -193,7 +193,8 @@ def __init__(self, storage):

# 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',{})

Expand Down Expand Up @@ -455,8 +456,8 @@ def decrypt_message(self, pubkey, message, password):
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):
Expand All @@ -465,9 +466,8 @@ def add_verified_tx(self, tx_hash, info):
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'''
Expand All @@ -488,34 +488,29 @@ 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

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)
Expand Down Expand Up @@ -827,7 +822,6 @@ def receive_history_callback(self, addr, hist):
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:
Expand All @@ -846,7 +840,6 @@ def receive_history_callback(self, addr, hist):
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)
Expand All @@ -865,19 +858,19 @@ def get_history(self, domain=None):

# 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()

# 3. add balance
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:
Expand Down Expand Up @@ -1076,7 +1069,7 @@ def prepare_for_verifier(self):
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:
Expand Down
2 changes: 1 addition & 1 deletion plugins/exchange_rate/qt.py
Expand Up @@ -217,7 +217,7 @@ def history_tab_update_begin(self):
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:
Expand Down

0 comments on commit 1a46a79

Please sign in to comment.