|
|
@ -42,7 +42,8 @@ from PyQt5.QtWidgets import (QMenu, QHeaderView, QLabel, QMessageBox, |
|
|
|
from electrum.address_synchronizer import TX_HEIGHT_LOCAL |
|
|
|
from electrum.i18n import _ |
|
|
|
from electrum.util import (block_explorer_URL, profiler, TxMinedInfo, |
|
|
|
OrderedDictWithIndex, timestamp_to_datetime) |
|
|
|
OrderedDictWithIndex, timestamp_to_datetime, |
|
|
|
Satoshis, format_time) |
|
|
|
from electrum.logging import get_logger, Logger |
|
|
|
|
|
|
|
from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton, |
|
|
@ -77,11 +78,11 @@ TX_ICONS = [ |
|
|
|
] |
|
|
|
|
|
|
|
class HistoryColumns(IntEnum): |
|
|
|
STATUS_ICON = 0 |
|
|
|
STATUS_TEXT = 1 |
|
|
|
DESCRIPTION = 2 |
|
|
|
COIN_VALUE = 3 |
|
|
|
RUNNING_COIN_BALANCE = 4 |
|
|
|
STATUS = 0 |
|
|
|
DESCRIPTION = 1 |
|
|
|
COIN_VALUE = 2 |
|
|
|
ONCHAIN_BALANCE = 3 |
|
|
|
CHANNELS_BALANCE = 4 |
|
|
|
FIAT_VALUE = 5 |
|
|
|
FIAT_ACQ_PRICE = 6 |
|
|
|
FIAT_CAP_GAINS = 7 |
|
|
@ -134,50 +135,66 @@ class HistoryModel(QAbstractItemModel, Logger): |
|
|
|
assert index.isValid() |
|
|
|
col = index.column() |
|
|
|
tx_item = self.transactions.value_from_pos(index.row()) |
|
|
|
tx_hash = tx_item['txid'] |
|
|
|
conf = tx_item['confirmations'] |
|
|
|
txpos = tx_item['txpos_in_block'] or 0 |
|
|
|
height = tx_item['height'] |
|
|
|
try: |
|
|
|
status, status_str = self.tx_status_cache[tx_hash] |
|
|
|
except KeyError: |
|
|
|
tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) |
|
|
|
status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) |
|
|
|
is_lightning = tx_item.get('lightning', False) |
|
|
|
timestamp = tx_item['timestamp'] |
|
|
|
if is_lightning: |
|
|
|
status = 0 |
|
|
|
status_str = format_time(int(timestamp)) |
|
|
|
else: |
|
|
|
tx_hash = tx_item['txid'] |
|
|
|
conf = tx_item['confirmations'] |
|
|
|
txpos = tx_item['txpos_in_block'] or 0 |
|
|
|
height = tx_item['height'] |
|
|
|
try: |
|
|
|
status, status_str = self.tx_status_cache[tx_hash] |
|
|
|
except KeyError: |
|
|
|
tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) |
|
|
|
status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) |
|
|
|
# we sort by timestamp |
|
|
|
if conf<=0: |
|
|
|
timestamp = time.time() |
|
|
|
|
|
|
|
if role == Qt.UserRole: |
|
|
|
# for sorting |
|
|
|
d = { |
|
|
|
HistoryColumns.STATUS_ICON: |
|
|
|
HistoryColumns.STATUS: |
|
|
|
# height breaks ties for unverified txns |
|
|
|
# txpos breaks ties for verified same block txns |
|
|
|
(status, conf, -height, -txpos), |
|
|
|
HistoryColumns.STATUS_TEXT: status_str, |
|
|
|
HistoryColumns.DESCRIPTION: tx_item['label'], |
|
|
|
HistoryColumns.COIN_VALUE: tx_item['value'].value, |
|
|
|
HistoryColumns.RUNNING_COIN_BALANCE: tx_item['balance'].value, |
|
|
|
(-timestamp, status, conf, -height, -txpos) if not is_lightning else (-timestamp, 0,0,0,0), |
|
|
|
HistoryColumns.DESCRIPTION: |
|
|
|
tx_item['label'] if 'label' in tx_item else None, |
|
|
|
HistoryColumns.COIN_VALUE: |
|
|
|
tx_item['value'].value if 'value' in tx_item else None, |
|
|
|
HistoryColumns.ONCHAIN_BALANCE: |
|
|
|
tx_item['balance'].value if not is_lightning else None, |
|
|
|
HistoryColumns.CHANNELS_BALANCE: |
|
|
|
tx_item['balance_msat'] if is_lightning else None, |
|
|
|
HistoryColumns.FIAT_VALUE: |
|
|
|
tx_item['fiat_value'].value if 'fiat_value' in tx_item else None, |
|
|
|
HistoryColumns.FIAT_ACQ_PRICE: |
|
|
|
tx_item['acquisition_price'].value if 'acquisition_price' in tx_item else None, |
|
|
|
HistoryColumns.FIAT_CAP_GAINS: |
|
|
|
tx_item['capital_gain'].value if 'capital_gain' in tx_item else None, |
|
|
|
HistoryColumns.TXID: tx_hash, |
|
|
|
HistoryColumns.TXID: tx_hash if not is_lightning else None, |
|
|
|
} |
|
|
|
return QVariant(d[col]) |
|
|
|
if role not in (Qt.DisplayRole, Qt.EditRole): |
|
|
|
if col == HistoryColumns.STATUS_ICON and role == Qt.DecorationRole: |
|
|
|
return QVariant(read_QIcon(TX_ICONS[status])) |
|
|
|
elif col == HistoryColumns.STATUS_ICON and role == Qt.ToolTipRole: |
|
|
|
return QVariant(str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))) |
|
|
|
if col == HistoryColumns.STATUS and role == Qt.DecorationRole: |
|
|
|
icon = "lightning" if is_lightning else TX_ICONS[status] |
|
|
|
return QVariant(read_QIcon(icon)) |
|
|
|
elif col == HistoryColumns.STATUS and role == Qt.ToolTipRole: |
|
|
|
msg = 'lightning transaction' if is_lightning else str(conf) + _(" confirmation" + ("s" if conf != 1 else "")) |
|
|
|
return QVariant(msg) |
|
|
|
elif col > HistoryColumns.DESCRIPTION and role == Qt.TextAlignmentRole: |
|
|
|
return QVariant(Qt.AlignRight | Qt.AlignVCenter) |
|
|
|
elif col != HistoryColumns.STATUS_TEXT and role == Qt.FontRole: |
|
|
|
elif col != HistoryColumns.STATUS and role == Qt.FontRole: |
|
|
|
monospace_font = QFont(MONOSPACE_FONT) |
|
|
|
return QVariant(monospace_font) |
|
|
|
elif col == HistoryColumns.DESCRIPTION and role == Qt.DecorationRole \ |
|
|
|
elif col == HistoryColumns.DESCRIPTION and role == Qt.DecorationRole and not is_lightning\ |
|
|
|
and self.parent.wallet.invoices.paid.get(tx_hash): |
|
|
|
return QVariant(read_QIcon("seal")) |
|
|
|
elif col in (HistoryColumns.DESCRIPTION, HistoryColumns.COIN_VALUE) \ |
|
|
|
and role == Qt.ForegroundRole and tx_item['value'].value < 0: |
|
|
|
and role == Qt.ForegroundRole and not is_lightning and tx_item['value'].value < 0: |
|
|
|
red_brush = QBrush(QColor("#BC1E1E")) |
|
|
|
return QVariant(red_brush) |
|
|
|
elif col == HistoryColumns.FIAT_VALUE and role == Qt.ForegroundRole \ |
|
|
@ -185,18 +202,22 @@ class HistoryModel(QAbstractItemModel, Logger): |
|
|
|
blue_brush = QBrush(QColor("#1E1EFF")) |
|
|
|
return QVariant(blue_brush) |
|
|
|
return QVariant() |
|
|
|
if col == HistoryColumns.STATUS_TEXT: |
|
|
|
if col == HistoryColumns.STATUS: |
|
|
|
return QVariant(status_str) |
|
|
|
elif col == HistoryColumns.DESCRIPTION: |
|
|
|
elif col == HistoryColumns.DESCRIPTION and 'label' in tx_item: |
|
|
|
return QVariant(tx_item['label']) |
|
|
|
elif col == HistoryColumns.COIN_VALUE: |
|
|
|
elif col == HistoryColumns.COIN_VALUE and 'value' in tx_item: |
|
|
|
value = tx_item['value'].value |
|
|
|
v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) |
|
|
|
return QVariant(v_str) |
|
|
|
elif col == HistoryColumns.RUNNING_COIN_BALANCE: |
|
|
|
elif col == HistoryColumns.ONCHAIN_BALANCE and not is_lightning: |
|
|
|
balance = tx_item['balance'].value |
|
|
|
balance_str = self.parent.format_amount(balance, whitespaces=True) |
|
|
|
return QVariant(balance_str) |
|
|
|
elif col == HistoryColumns.CHANNELS_BALANCE and is_lightning: |
|
|
|
balance = tx_item['balance_msat']//1000 |
|
|
|
balance_str = self.parent.format_amount(balance, whitespaces=True) |
|
|
|
return QVariant(balance_str) |
|
|
|
elif col == HistoryColumns.FIAT_VALUE and 'fiat_value' in tx_item: |
|
|
|
value_str = self.parent.fx.format_fiat(tx_item['fiat_value'].value) |
|
|
|
return QVariant(value_str) |
|
|
@ -240,18 +261,24 @@ class HistoryModel(QAbstractItemModel, Logger): |
|
|
|
fx = self.parent.fx |
|
|
|
if fx: fx.history_used_spot = False |
|
|
|
r = self.parent.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) |
|
|
|
lightning_history = self.parent.wallet.lnworker.get_history() |
|
|
|
self.set_visibility_of_columns() |
|
|
|
if r['transactions'] == list(self.transactions.values()): |
|
|
|
return |
|
|
|
#if r['transactions'] == list(self.transactions.values()): |
|
|
|
# return |
|
|
|
old_length = len(self.transactions) |
|
|
|
if old_length != 0: |
|
|
|
self.beginRemoveRows(QModelIndex(), 0, old_length) |
|
|
|
self.transactions.clear() |
|
|
|
self.endRemoveRows() |
|
|
|
self.beginInsertRows(QModelIndex(), 0, len(r['transactions'])-1) |
|
|
|
self.beginInsertRows(QModelIndex(), 0, len(r['transactions'])+len(lightning_history)-1) |
|
|
|
for tx_item in r['transactions']: |
|
|
|
txid = tx_item['txid'] |
|
|
|
self.transactions[txid] = tx_item |
|
|
|
for tx_item in lightning_history: |
|
|
|
tx_item['lightning'] = True |
|
|
|
tx_item['value'] = Satoshis(tx_item['amount_msat']/1000 * (-1 if tx_item['direction'] =='sent' else 1)) |
|
|
|
key = tx_item['payment_hash'] if 'payment_hash' in tx_item else tx_item['type'] + tx_item['channel_id'] |
|
|
|
self.transactions[key] = tx_item |
|
|
|
self.endInsertRows() |
|
|
|
if selected_row: |
|
|
|
self.view.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent) |
|
|
@ -269,8 +296,9 @@ class HistoryModel(QAbstractItemModel, Logger): |
|
|
|
# update tx_status_cache |
|
|
|
self.tx_status_cache.clear() |
|
|
|
for txid, tx_item in self.transactions.items(): |
|
|
|
tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) |
|
|
|
self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_info) |
|
|
|
if not tx_item.get('lightning', False): |
|
|
|
tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) |
|
|
|
self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_info) |
|
|
|
|
|
|
|
def set_visibility_of_columns(self): |
|
|
|
def set_visible(col: int, b: bool): |
|
|
@ -312,6 +340,8 @@ class HistoryModel(QAbstractItemModel, Logger): |
|
|
|
|
|
|
|
def on_fee_histogram(self): |
|
|
|
for tx_hash, tx_item in list(self.transactions.items()): |
|
|
|
if tx_item.get('lightning'): |
|
|
|
continue |
|
|
|
tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) |
|
|
|
if tx_mined_info.conf > 0: |
|
|
|
# note: we could actually break here if we wanted to rely on the order of txns in self.transactions |
|
|
@ -331,11 +361,11 @@ class HistoryModel(QAbstractItemModel, Logger): |
|
|
|
fiat_acq_title = '%s '%fx.ccy + _('Acquisition price') |
|
|
|
fiat_cg_title = '%s '%fx.ccy + _('Capital Gains') |
|
|
|
return { |
|
|
|
HistoryColumns.STATUS_ICON: '', |
|
|
|
HistoryColumns.STATUS_TEXT: _('Date'), |
|
|
|
HistoryColumns.STATUS: _('Date'), |
|
|
|
HistoryColumns.DESCRIPTION: _('Description'), |
|
|
|
HistoryColumns.COIN_VALUE: _('Amount'), |
|
|
|
HistoryColumns.RUNNING_COIN_BALANCE: _('Balance'), |
|
|
|
HistoryColumns.ONCHAIN_BALANCE: _('Balance'), |
|
|
|
HistoryColumns.CHANNELS_BALANCE: u'\U0001f5f2 ' + _('Channels balance'), |
|
|
|
HistoryColumns.FIAT_VALUE: fiat_title, |
|
|
|
HistoryColumns.FIAT_ACQ_PRICE: fiat_acq_title, |
|
|
|
HistoryColumns.FIAT_CAP_GAINS: fiat_cg_title, |
|
|
@ -356,7 +386,7 @@ class HistoryModel(QAbstractItemModel, Logger): |
|
|
|
return tx_mined_info |
|
|
|
|
|
|
|
class HistoryList(MyTreeView, AcceptFileDragDrop): |
|
|
|
filter_columns = [HistoryColumns.STATUS_TEXT, |
|
|
|
filter_columns = [HistoryColumns.STATUS, |
|
|
|
HistoryColumns.DESCRIPTION, |
|
|
|
HistoryColumns.COIN_VALUE, |
|
|
|
HistoryColumns.TXID] |
|
|
@ -390,7 +420,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): |
|
|
|
self.years = [] |
|
|
|
self.create_toolbar_buttons() |
|
|
|
self.wallet = self.parent.wallet # type: Abstract_Wallet |
|
|
|
self.sortByColumn(HistoryColumns.STATUS_ICON, Qt.AscendingOrder) |
|
|
|
self.sortByColumn(HistoryColumns.STATUS, Qt.AscendingOrder) |
|
|
|
self.editable_columns |= {HistoryColumns.FIAT_VALUE} |
|
|
|
|
|
|
|
self.header().setStretchLastSection(False) |
|
|
@ -565,12 +595,10 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): |
|
|
|
return |
|
|
|
tx_item = self.hm.transactions.value_from_pos(idx.row()) |
|
|
|
column = idx.column() |
|
|
|
if column == HistoryColumns.STATUS_ICON: |
|
|
|
column_title = _('Transaction ID') |
|
|
|
column_data = tx_item['txid'] |
|
|
|
else: |
|
|
|
column_title = self.hm.headerData(column, Qt.Horizontal, Qt.DisplayRole) |
|
|
|
column_data = self.hm.data(idx, Qt.DisplayRole).value() |
|
|
|
column_title = self.hm.headerData(column, Qt.Horizontal, Qt.DisplayRole) |
|
|
|
column_data = self.hm.data(idx, Qt.DisplayRole).value() |
|
|
|
if tx_item.get('lightning'): |
|
|
|
return |
|
|
|
tx_hash = tx_item['txid'] |
|
|
|
tx = self.wallet.db.get_transaction(tx_hash) |
|
|
|
if not tx: |
|
|
@ -583,12 +611,13 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): |
|
|
|
menu = QMenu() |
|
|
|
if height == TX_HEIGHT_LOCAL: |
|
|
|
menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) |
|
|
|
menu.addAction(_("Copy Transaction ID"), lambda: self.parent.app.clipboard().setText(tx_hash)) |
|
|
|
|
|
|
|
amount_columns = [HistoryColumns.COIN_VALUE, HistoryColumns.RUNNING_COIN_BALANCE, HistoryColumns.FIAT_VALUE, HistoryColumns.FIAT_ACQ_PRICE, HistoryColumns.FIAT_CAP_GAINS] |
|
|
|
amount_columns = [HistoryColumns.COIN_VALUE, HistoryColumns.ONCHAIN_BALANCE, HistoryColumns.CHANNELS_BALANCE, |
|
|
|
HistoryColumns.FIAT_VALUE, HistoryColumns.FIAT_ACQ_PRICE, HistoryColumns.FIAT_CAP_GAINS] |
|
|
|
if column in amount_columns: |
|
|
|
column_data = column_data.strip() |
|
|
|
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) |
|
|
|
|
|
|
|
for c in self.editable_columns: |
|
|
|
if self.isColumnHidden(c): continue |
|
|
|
label = self.hm.headerData(c, Qt.Horizontal, Qt.DisplayRole) |
|
|
|