show warning icon if unconfirmed tx has low fee. fixes 1798
This commit is contained in:
parent
bce42cb496
commit
599906eef6
|
@ -88,6 +88,20 @@ class CScreen(Factory.Screen):
|
||||||
self.add_widget(self.context_menu)
|
self.add_widget(self.context_menu)
|
||||||
|
|
||||||
|
|
||||||
|
TX_ICONS = [
|
||||||
|
"close",
|
||||||
|
"close",
|
||||||
|
"close",
|
||||||
|
"unconfirmed",
|
||||||
|
"close",
|
||||||
|
"clock1",
|
||||||
|
"clock2",
|
||||||
|
"clock3",
|
||||||
|
"clock4",
|
||||||
|
"clock5",
|
||||||
|
"confirmed",
|
||||||
|
]
|
||||||
|
|
||||||
class HistoryScreen(CScreen):
|
class HistoryScreen(CScreen):
|
||||||
|
|
||||||
tab = ObjectProperty(None)
|
tab = ObjectProperty(None)
|
||||||
|
@ -119,30 +133,8 @@ class HistoryScreen(CScreen):
|
||||||
def parse_history(self, items):
|
def parse_history(self, items):
|
||||||
for item in items:
|
for item in items:
|
||||||
tx_hash, height, conf, timestamp, value, balance = item
|
tx_hash, height, conf, timestamp, value, balance = item
|
||||||
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] if timestamp else _("unknown")
|
status, status_str = self.app.wallet.get_tx_status(tx_hash, height, conf, timestamp)
|
||||||
if conf == 0:
|
icon = "atlas://gui/kivy/theming/light/" + TX_ICONS[status]
|
||||||
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 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)
|
|
||||||
else:
|
|
||||||
icon = "atlas://gui/kivy/theming/light/confirmed"
|
|
||||||
|
|
||||||
label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs')
|
label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs')
|
||||||
date = timestamp_to_datetime(timestamp)
|
date = timestamp_to_datetime(timestamp)
|
||||||
quote_text = ''
|
quote_text = ''
|
||||||
|
@ -151,7 +143,7 @@ class HistoryScreen(CScreen):
|
||||||
if rate:
|
if rate:
|
||||||
s = run_hook('value_str', value, rate)
|
s = run_hook('value_str', value, rate)
|
||||||
quote_text = '' if s is None else s + ' ' + self.app.fiat_unit
|
quote_text = '' if s is None else s + ' ' + self.app.fiat_unit
|
||||||
yield (conf, icon, time_str, label, value, tx_hash, quote_text)
|
yield (conf, icon, status_str, label, value, tx_hash, quote_text)
|
||||||
|
|
||||||
def update(self, see_all=False):
|
def update(self, see_all=False):
|
||||||
if self.app.wallet is None:
|
if self.app.wallet is None:
|
||||||
|
|
|
@ -32,6 +32,21 @@ from electrum.util import block_explorer_URL, format_satoshis, format_time
|
||||||
from electrum.plugins import run_hook
|
from electrum.plugins import run_hook
|
||||||
|
|
||||||
|
|
||||||
|
TX_ICONS = [
|
||||||
|
"warning.png",
|
||||||
|
"warning.png",
|
||||||
|
"warning.png",
|
||||||
|
"unconfirmed.png",
|
||||||
|
"unconfirmed.png",
|
||||||
|
"clock1.png",
|
||||||
|
"clock2.png",
|
||||||
|
"clock3.png",
|
||||||
|
"clock4.png",
|
||||||
|
"clock5.png",
|
||||||
|
"confirmed.png",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class HistoryList(MyTreeWidget):
|
class HistoryList(MyTreeWidget):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
|
@ -40,31 +55,10 @@ class HistoryList(MyTreeWidget):
|
||||||
self.setColumnHidden(1, True)
|
self.setColumnHidden(1, True)
|
||||||
|
|
||||||
def refresh_headers(self):
|
def refresh_headers(self):
|
||||||
headers = ['', '', _('Date'), _('Description') , _('Amount'),
|
headers = ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')]
|
||||||
_('Balance')]
|
|
||||||
run_hook('history_tab_headers', headers)
|
run_hook('history_tab_headers', headers)
|
||||||
self.update_headers(headers)
|
self.update_headers(headers)
|
||||||
|
|
||||||
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 = _('Not Verified')
|
|
||||||
icon = QIcon(":icons/unconfirmed.png")
|
|
||||||
elif conf < 6:
|
|
||||||
icon = QIcon(":icons/clock%d.png"%conf)
|
|
||||||
else:
|
|
||||||
icon = QIcon(":icons/confirmed.png")
|
|
||||||
return icon, time_str
|
|
||||||
|
|
||||||
def get_domain(self):
|
def get_domain(self):
|
||||||
'''Replaced in address_dialog.py'''
|
'''Replaced in address_dialog.py'''
|
||||||
return self.wallet.get_account_addresses(self.parent.current_account)
|
return self.wallet.get_account_addresses(self.parent.current_account)
|
||||||
|
@ -78,16 +72,12 @@ class HistoryList(MyTreeWidget):
|
||||||
run_hook('history_tab_update_begin')
|
run_hook('history_tab_update_begin')
|
||||||
for h_item in h:
|
for h_item in h:
|
||||||
tx_hash, height, conf, timestamp, value, balance = h_item
|
tx_hash, height, conf, timestamp, value, balance = h_item
|
||||||
if conf == 0:
|
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
|
||||||
tx = self.wallet.transactions.get(tx_hash)
|
icon = QIcon(":icons/" + TX_ICONS[status])
|
||||||
is_final = tx and 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)
|
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)
|
||||||
entry = ['', tx_hash, time_str, label, v_str, balance_str]
|
entry = ['', tx_hash, status_str, label, v_str, balance_str]
|
||||||
run_hook('history_tab_update', h_item, entry)
|
run_hook('history_tab_update', h_item, entry)
|
||||||
item = QTreeWidgetItem(entry)
|
item = QTreeWidgetItem(entry)
|
||||||
item.setIcon(0, icon)
|
item.setIcon(0, icon)
|
||||||
|
@ -106,7 +96,8 @@ class HistoryList(MyTreeWidget):
|
||||||
self.setCurrentItem(item)
|
self.setCurrentItem(item)
|
||||||
|
|
||||||
def update_item(self, tx_hash, height, conf, timestamp):
|
def update_item(self, tx_hash, height, conf, timestamp):
|
||||||
icon, time_str = self.get_icon(height, conf, timestamp, True)
|
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
|
||||||
|
icon = QIcon(":icons/" + TX_ICONS[status])
|
||||||
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]
|
||||||
|
|
|
@ -111,6 +111,9 @@ class Synchronizer(ThreadJob):
|
||||||
server_status = self.requested_histories[addr]
|
server_status = self.requested_histories[addr]
|
||||||
hashes = set(map(lambda item: item['tx_hash'], result))
|
hashes = set(map(lambda item: item['tx_hash'], result))
|
||||||
hist = map(lambda item: (item['tx_hash'], item['height']), result)
|
hist = map(lambda item: (item['tx_hash'], item['height']), result)
|
||||||
|
# tx_fees
|
||||||
|
tx_fees = [(item['tx_hash'], item.get('fee')) for item in result]
|
||||||
|
tx_fees = dict(filter(lambda x:x[1] is not None, tx_fees))
|
||||||
# Note if the server hasn't been patched to sort the items properly
|
# Note if the server hasn't been patched to sort the items properly
|
||||||
if hist != sorted(hist, key=lambda x:x[1]):
|
if hist != sorted(hist, key=lambda x:x[1]):
|
||||||
self.network.interface.print_error("serving improperly sorted address histories")
|
self.network.interface.print_error("serving improperly sorted address histories")
|
||||||
|
@ -122,7 +125,7 @@ class Synchronizer(ThreadJob):
|
||||||
self.print_error("error: status mismatch: %s" % addr)
|
self.print_error("error: status mismatch: %s" % addr)
|
||||||
else:
|
else:
|
||||||
# Store received history
|
# Store received history
|
||||||
self.wallet.receive_history_callback(addr, hist)
|
self.wallet.receive_history_callback(addr, hist, tx_fees)
|
||||||
# Request transactions we don't have
|
# Request transactions we don't have
|
||||||
self.request_missing_txs(hist)
|
self.request_missing_txs(hist)
|
||||||
# Remove request; this allows up_to_date to be True
|
# Remove request; this allows up_to_date to be True
|
||||||
|
|
|
@ -57,6 +57,16 @@ import paymentrequest
|
||||||
# internal ID for imported account
|
# internal ID for imported account
|
||||||
IMPORTED_ACCOUNT = '/x'
|
IMPORTED_ACCOUNT = '/x'
|
||||||
|
|
||||||
|
|
||||||
|
TX_STATUS = [
|
||||||
|
_('Replaceable'),
|
||||||
|
_('Unconfirmed parent'),
|
||||||
|
_('Low fee'),
|
||||||
|
_('Unconfirmed'),
|
||||||
|
_('Not Verified'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class WalletStorage(PrintError):
|
class WalletStorage(PrintError):
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
|
@ -225,6 +235,7 @@ class Abstract_Wallet(PrintError):
|
||||||
def load_transactions(self):
|
def load_transactions(self):
|
||||||
self.txi = self.storage.get('txi', {})
|
self.txi = self.storage.get('txi', {})
|
||||||
self.txo = self.storage.get('txo', {})
|
self.txo = self.storage.get('txo', {})
|
||||||
|
self.tx_fees = self.storage.get('tx_fees', {})
|
||||||
self.pruned_txo = self.storage.get('pruned_txo', {})
|
self.pruned_txo = self.storage.get('pruned_txo', {})
|
||||||
tx_list = self.storage.get('transactions', {})
|
tx_list = self.storage.get('transactions', {})
|
||||||
self.transactions = {}
|
self.transactions = {}
|
||||||
|
@ -244,6 +255,7 @@ class Abstract_Wallet(PrintError):
|
||||||
self.storage.put('transactions', tx)
|
self.storage.put('transactions', tx)
|
||||||
self.storage.put('txi', self.txi)
|
self.storage.put('txi', self.txi)
|
||||||
self.storage.put('txo', self.txo)
|
self.storage.put('txo', self.txo)
|
||||||
|
self.storage.put('tx_fees', self.tx_fees)
|
||||||
self.storage.put('pruned_txo', self.pruned_txo)
|
self.storage.put('pruned_txo', self.pruned_txo)
|
||||||
self.storage.put('addr_history', self.history)
|
self.storage.put('addr_history', self.history)
|
||||||
if write:
|
if write:
|
||||||
|
@ -253,6 +265,7 @@ class Abstract_Wallet(PrintError):
|
||||||
with self.transaction_lock:
|
with self.transaction_lock:
|
||||||
self.txi = {}
|
self.txi = {}
|
||||||
self.txo = {}
|
self.txo = {}
|
||||||
|
self.tx_fees = {}
|
||||||
self.pruned_txo = {}
|
self.pruned_txo = {}
|
||||||
self.save_transactions()
|
self.save_transactions()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -804,8 +817,7 @@ class Abstract_Wallet(PrintError):
|
||||||
self.save_transactions()
|
self.save_transactions()
|
||||||
self.add_unverified_tx(tx_hash, tx_height)
|
self.add_unverified_tx(tx_hash, tx_height)
|
||||||
|
|
||||||
|
def receive_history_callback(self, addr, hist, tx_fees):
|
||||||
def receive_history_callback(self, addr, hist):
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
old_hist = self.history.get(addr, [])
|
old_hist = self.history.get(addr, [])
|
||||||
for tx_hash, height in old_hist:
|
for tx_hash, height in old_hist:
|
||||||
|
@ -830,6 +842,8 @@ class Abstract_Wallet(PrintError):
|
||||||
|
|
||||||
# Write updated TXI, TXO etc.
|
# Write updated TXI, TXO etc.
|
||||||
self.save_transactions()
|
self.save_transactions()
|
||||||
|
# Store fees
|
||||||
|
self.tx_fees.update(tx_fees)
|
||||||
|
|
||||||
def get_history(self, domain=None):
|
def get_history(self, domain=None):
|
||||||
# get domain
|
# get domain
|
||||||
|
@ -905,6 +919,34 @@ class Abstract_Wallet(PrintError):
|
||||||
else:
|
else:
|
||||||
return F
|
return F
|
||||||
|
|
||||||
|
def get_tx_status(self, tx_hash, height, conf, timestamp):
|
||||||
|
from util import format_time
|
||||||
|
if conf == 0:
|
||||||
|
tx = self.transactions.get(tx_hash)
|
||||||
|
is_final = tx and tx.is_final()
|
||||||
|
fee = self.tx_fees.get(tx_hash)
|
||||||
|
if fee and self.network and self.network.fee:
|
||||||
|
size = len(tx.raw)/2
|
||||||
|
network_fee = int(self.network.fee * size / 1000)
|
||||||
|
is_lowfee = fee < network_fee * 0.25
|
||||||
|
else:
|
||||||
|
is_lowfee = False
|
||||||
|
if not is_final:
|
||||||
|
status = 0
|
||||||
|
elif height < 0:
|
||||||
|
status = 1
|
||||||
|
elif height == 0 and is_lowfee:
|
||||||
|
status = 2
|
||||||
|
elif height == 0:
|
||||||
|
status = 3
|
||||||
|
else:
|
||||||
|
status = 4
|
||||||
|
else:
|
||||||
|
status = 4 + min(conf, 6)
|
||||||
|
time_str = format_time(timestamp) if timestamp else _("unknown")
|
||||||
|
status_str = TX_STATUS[status] if status < 5 else time_str
|
||||||
|
return status, status_str
|
||||||
|
|
||||||
def relayfee(self):
|
def relayfee(self):
|
||||||
RELAY_FEE = 5000
|
RELAY_FEE = 5000
|
||||||
MAX_RELAY_FEE = 50000
|
MAX_RELAY_FEE = 50000
|
||||||
|
|
Loading…
Reference in New Issue