Browse Source

Rework wallet history methods:

- wallet.get_full_history returns onchain and lightning
 - capital gains are returned by get_detailed_history
 - display lightning history in kivy
 - command line: separate lightning and onchain history
dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
ThomasV 6 years ago
parent
commit
af7d7e883c
  1. 15
      electrum/commands.py
  2. 47
      electrum/gui/kivy/uix/screens.py
  3. 39
      electrum/gui/qt/history_list.py
  4. 98
      electrum/wallet.py

15
electrum/commands.py

@ -497,14 +497,11 @@ class Commands:
return tx.as_dict()
@command('w')
def history(self, year=None, show_addresses=False, show_fiat=False, show_fees=False,
from_height=None, to_height=None):
"""Wallet history. Returns the transaction history of your wallet."""
def onchain_history(self, year=None, show_addresses=False, show_fiat=False, show_fees=False):
"""Wallet onchain history. Returns the transaction history of your wallet."""
kwargs = {
'show_addresses': show_addresses,
'show_fees': show_fees,
'from_height': from_height,
'to_height': to_height,
}
if year:
import time
@ -516,7 +513,13 @@ class Commands:
from .exchange_rate import FxThread
fx = FxThread(self.config, None)
kwargs['fx'] = fx
return json_encode(self.wallet.get_full_history(**kwargs))
return json_encode(self.wallet.get_detailed_history(**kwargs))
@command('w')
def lightning_history(self, show_fiat=False):
""" lightning history """
lightning_history = self.wallet.lnworker.get_history() if self.wallet.lnworker else []
return json_encode(lightning_history)
@command('w')
def setlabel(self, key, label):

47
electrum/gui/kivy/uix/screens.py

@ -26,7 +26,7 @@ from electrum.util import profiler, parse_URI, format_time, InvalidPassword, Not
from electrum import bitcoin, constants
from electrum.transaction import TxOutput, Transaction, tx_from_str
from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI
from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED, TxMinedInfo
from electrum.plugin import run_hook
from electrum.wallet import InternalAddressCorruption
from electrum import simple_config
@ -145,34 +145,49 @@ class HistoryScreen(CScreen):
d = LabelDialog(_('Enter Transaction Label'), text, callback)
d.open()
def get_card(self, tx_hash, tx_mined_status, value, balance):
status, status_str = self.app.wallet.get_tx_status(tx_hash, tx_mined_status)
icon = "atlas://electrum/gui/kivy/theming/light/" + TX_ICONS[status]
label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs')
def get_card(self, tx_item): #tx_hash, tx_mined_status, value, balance):
is_lightning = tx_item.get('lightning', False)
timestamp = tx_item['timestamp']
if is_lightning:
status = 0
txpos = tx_item['txpos']
if timestamp is None:
status_str = 'unconfirmed'
else:
status_str = format_time(int(timestamp))
icon = "atlas://electrum/gui/kivy/theming/light/lightning"
else:
tx_hash = tx_item['txid']
conf = tx_item['confirmations']
txpos = tx_item['txpos_in_block'] or 0
height = tx_item['height']
tx_mined_info = TxMinedInfo(height=tx_item['height'],
conf=tx_item['confirmations'],
timestamp=tx_item['timestamp'])
status, status_str = self.app.wallet.get_tx_status(tx_hash, tx_mined_info)
icon = "atlas://electrum/gui/kivy/theming/light/" + TX_ICONS[status]
ri = {}
ri['screen'] = self
ri['tx_hash'] = tx_hash
ri['icon'] = icon
ri['date'] = status_str
ri['message'] = label
ri['confirmations'] = tx_mined_status.conf
ri['message'] = tx_item['label']
value = tx_item['value'].value
if value is not None:
ri['is_mine'] = value < 0
if value < 0: value = - value
ri['amount'] = self.app.format_amount_and_units(value)
if self.app.fiat_unit:
fx = self.app.fx
fiat_value = value / Decimal(bitcoin.COIN) * self.app.wallet.price_at_timestamp(tx_hash, fx.timestamp_rate)
fiat_value = Fiat(fiat_value, fx.ccy)
ri['quote_text'] = fiat_value.to_ui_string()
if 'fiat_value' in tx_item:
ri['quote_text'] = tx_item['fiat_value'].to_ui_string()
return ri
def update(self, see_all=False):
if self.app.wallet is None:
import operator
wallet = self.app.wallet
if wallet is None:
return
history = reversed(self.app.wallet.get_history())
history = sorted(wallet.get_full_history(self.app.fx).values(), key=lambda x: x.get('timestamp') or float('inf'), reverse=True)
history_card = self.screen.ids.history_container
history_card.data = [self.get_card(*item) for item in history]
history_card.data = [self.get_card(item) for item in history]
class SendScreen(CScreen):

39
electrum/gui/qt/history_list.py

@ -116,7 +116,6 @@ class HistoryModel(QAbstractItemModel, Logger):
self.view = None # type: HistoryList
self.transactions = OrderedDictWithIndex()
self.tx_status_cache = {} # type: Dict[str, Tuple[int, str]]
self.summary = None
def set_view(self, history_list: 'HistoryList'):
# FIXME HistoryModel and HistoryList mutually depend on each other.
@ -173,7 +172,7 @@ class HistoryModel(QAbstractItemModel, Logger):
HistoryColumns.DESCRIPTION:
tx_item['label'] if 'label' in tx_item else None,
HistoryColumns.AMOUNT:
tx_item['value'].value if 'value' in tx_item else None,
tx_item['bc_value'].value if 'bc_value' in tx_item else None,
HistoryColumns.LN_AMOUNT:
tx_item['ln_value'].value if 'ln_value' in tx_item else None,
HistoryColumns.BALANCE:
@ -217,8 +216,8 @@ class HistoryModel(QAbstractItemModel, Logger):
return QVariant(status_str)
elif col == HistoryColumns.DESCRIPTION and 'label' in tx_item:
return QVariant(tx_item['label'])
elif col == HistoryColumns.AMOUNT and 'value' in tx_item:
value = tx_item['value'].value
elif col == HistoryColumns.AMOUNT and 'bc_value' in tx_item:
value = tx_item['bc_value'].value
v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True)
return QVariant(v_str)
elif col == HistoryColumns.LN_AMOUNT and 'ln_value' in tx_item:
@ -276,44 +275,22 @@ class HistoryModel(QAbstractItemModel, Logger):
fx = self.parent.fx
if fx: fx.history_used_spot = False
wallet = self.parent.wallet
r = wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx)
lightning_history = wallet.lnworker.get_history() if wallet.lnworker else []
self.set_visibility_of_columns()
#if r['transactions'] == list(self.transactions.values()):
# return
transactions = wallet.get_full_history(self.parent.fx)
if 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()
transactions = OrderedDictWithIndex()
for tx_item in r['transactions']:
txid = tx_item['txid']
transactions[txid] = tx_item
for i, tx_item in enumerate(lightning_history):
txid = tx_item.get('txid')
ln_value = tx_item['amount_msat']/1000.
if txid and txid in transactions:
item = transactions[txid]
item['label'] = tx_item['label']
item['ln_value'] = Satoshis(ln_value)
item['balance_msat'] = tx_item['balance_msat']
else:
tx_item['lightning'] = True
tx_item['ln_value'] = Satoshis(ln_value)
tx_item['txpos'] = i # for sorting
key = tx_item['payment_hash'] if 'payment_hash' in tx_item else tx_item['type'] + tx_item['channel_id']
transactions[key] = tx_item
self.beginInsertRows(QModelIndex(), 0, len(transactions)-1)
self.transactions = transactions
self.endInsertRows()
if selected_row:
self.view.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent)
self.view.filter()
# update summary
self.summary = r['summary']
# update time filter
if not self.view.years and self.transactions:
start_date = date.today()
end_date = date.today()
@ -535,7 +512,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
return datetime.datetime(date.year, date.month, date.day)
def show_summary(self):
h = self.model().sourceModel().summary
h = self.parent.wallet.get_detailed_history()['summary']
if not h:
self.parent.show_message(_("Nothing to summarize."))
return

98
electrum/wallet.py

@ -45,7 +45,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
WalletFileException, BitcoinException,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri)
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
from .simple_config import get_config
from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
is_minikey, relayfee, dust_threshold)
@ -482,45 +482,79 @@ class Abstract_Wallet(AddressSynchronizer):
# return last balance
return balance
def get_onchain_history(self):
for tx_hash, tx_mined_status, value, balance in self.get_history():
yield {
'txid': tx_hash,
'height': tx_mined_status.height,
'confirmations': tx_mined_status.conf,
'timestamp': tx_mined_status.timestamp,
'incoming': True if value>0 else False,
'bc_value': Satoshis(value),
'balance': Satoshis(balance),
'date': timestamp_to_datetime(tx_mined_status.timestamp),
'label': self.get_label(tx_hash),
'txpos_in_block': tx_mined_status.txpos,
}
@profiler
def get_full_history(self, fx=None):
transactions = OrderedDictWithIndex()
onchain_history = self.get_onchain_history()
for tx_item in onchain_history:
txid = tx_item['txid']
transactions[txid] = tx_item
lightning_history = self.lnworker.get_history() if self.lnworker else []
for i, tx_item in enumerate(lightning_history):
txid = tx_item.get('txid')
ln_value = Decimal(tx_item['amount_msat']) / 1000
if txid and txid in transactions:
item = transactions[txid]
item['label'] = tx_item['label']
item['ln_value'] = Satoshis(ln_value)
item['balance_msat'] = tx_item['balance_msat']
else:
tx_item['lightning'] = True
tx_item['ln_value'] = Satoshis(ln_value)
tx_item['txpos'] = i # for sorting
key = tx_item['payment_hash'] if 'payment_hash' in tx_item else tx_item['type'] + tx_item['channel_id']
transactions[key] = tx_item
now = time.time()
for item in transactions.values():
# add on-chain and lightning values
value = Decimal(0)
if item.get('bc_value'):
value += item['bc_value'].value
if item.get('ln_value'):
value += item.get('ln_value').value
item['value'] = Satoshis(value)
if fx:
timestamp = item['timestamp'] or now
fiat_value = value / Decimal(bitcoin.COIN) * fx.timestamp_rate(timestamp)
item['fiat_value'] = Fiat(fiat_value, fx.ccy)
item['fiat_default'] = True
return transactions
@profiler
def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None,
fx=None, show_addresses=False, show_fees=False,
from_height=None, to_height=None):
if (from_timestamp is not None or to_timestamp is not None) \
and (from_height is not None or to_height is not None):
raise Exception('timestamp and block height based filtering cannot be used together')
def get_detailed_history(self, from_timestamp=None, to_timestamp=None,
fx=None, show_addresses=False, show_fees=False):
# History with capital gains, using utxo pricing
# FIXME: Lightning capital gains would requires FIFO
out = []
income = 0
expenditures = 0
capital_gains = Decimal(0)
fiat_income = Decimal(0)
fiat_expenditures = Decimal(0)
h = self.get_history(domain)
now = time.time()
for tx_hash, tx_mined_status, value, balance in h:
timestamp = tx_mined_status.timestamp
for item in self.get_onchain_history():
timestamp = item['timestamp']
if from_timestamp and (timestamp or now) < from_timestamp:
continue
if to_timestamp and (timestamp or now) >= to_timestamp:
continue
height = tx_mined_status.height
if from_height is not None and height < from_height:
continue
if to_height is not None and height >= to_height:
continue
tx_hash = item['txid']
tx = self.db.get_transaction(tx_hash)
item = {
'txid': tx_hash,
'height': height,
'confirmations': tx_mined_status.conf,
'timestamp': timestamp,
'incoming': True if value>0 else False,
'value': Satoshis(value),
'balance': Satoshis(balance),
'date': timestamp_to_datetime(timestamp),
'label': self.get_label(tx_hash),
'txpos_in_block': tx_mined_status.txpos,
}
tx_fee = None
if show_fees:
tx_fee = self.get_tx_fee(tx)
@ -529,10 +563,8 @@ class Abstract_Wallet(AddressSynchronizer):
item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs()))
item['outputs'] = list(map(lambda x:{'address':x.address, 'value':Satoshis(x.value)},
tx.get_outputs_for_UI()))
# value may be None if wallet is not fully synchronized
if value is None:
continue
# fixme: use in and out values
value = item['bc_value'].value
if value < 0:
expenditures += -value
else:
@ -550,7 +582,7 @@ class Abstract_Wallet(AddressSynchronizer):
out.append(item)
# add summary
if out:
b, v = out[0]['balance'].value, out[0]['value'].value
b, v = out[0]['balance'].value, out[0]['bc_value'].value
start_balance = None if b is None or v is None else b - v
end_balance = out[-1]['balance'].value
if from_timestamp is not None and to_timestamp is not None:
@ -562,15 +594,13 @@ class Abstract_Wallet(AddressSynchronizer):
summary = {
'start_date': start_date,
'end_date': end_date,
'from_height': from_height,
'to_height': to_height,
'start_balance': Satoshis(start_balance),
'end_balance': Satoshis(end_balance),
'incoming': Satoshis(income),
'outgoing': Satoshis(expenditures)
}
if fx and fx.is_enabled() and fx.get_history_config():
unrealized = self.unrealized_gains(domain, fx.timestamp_rate, fx.ccy)
unrealized = self.unrealized_gains(None, fx.timestamp_rate, fx.ccy)
summary['fiat_currency'] = fx.ccy
summary['fiat_capital_gains'] = Fiat(capital_gains, fx.ccy)
summary['fiat_incoming'] = Fiat(fiat_income, fx.ccy)

Loading…
Cancel
Save