electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit af7d7e883c760caedbe8ba738e450fae535b1886
parent 7e8be3d2e7e178cc6534520c07d0af413896f2bf
Author: ThomasV <thomasv@electrum.org>
Date:   Fri, 14 Jun 2019 13:01:23 +0200

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

Diffstat:
Melectrum/commands.py | 15+++++++++------
Melectrum/gui/kivy/uix/screens.py | 47+++++++++++++++++++++++++++++++----------------
Melectrum/gui/qt/history_list.py | 39++++++++-------------------------------
Melectrum/wallet.py | 98+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
4 files changed, 112 insertions(+), 87 deletions(-)

diff --git a/electrum/commands.py b/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): diff --git a/electrum/gui/kivy/uix/screens.py b/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): diff --git a/electrum/gui/qt/history_list.py b/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 diff --git a/electrum/wallet.py b/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)