electrum

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

commit e3de121be9a56c84ec80ce79a84f7ce1ae6b9b20
parent d9b1271f6571ea3f93519245c8405d249c7fd548
Author: ThomasV <thomasv@gitorious>
Date:   Fri, 20 Mar 2015 22:37:06 +0100

store deserialized tx in/out in wallet file for fast computation

Diffstat:
Mgui/gtk.py | 4++--
Mgui/qt/lite_window.py | 4++--
Mgui/qt/main_window.py | 44++++++++++++++++++++++----------------------
Mgui/qt/transaction_dialog.py | 11+++++------
Mgui/text.py | 2+-
Mlib/commands.py | 2+-
Mlib/synchronizer.py | 65++++++++++++++++++++++++++++++++++-------------------------------
Mlib/transaction.py | 93+++++++++++++------------------------------------------------------------------
Mlib/wallet.py | 478+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mplugins/exchange_rate.py | 15++++++++-------
Mplugins/plot.py | 42++++++++++++++----------------------------
11 files changed, 390 insertions(+), 370 deletions(-)

diff --git a/gui/gtk.py b/gui/gtk.py @@ -1170,7 +1170,7 @@ class ElectrumWindow: cursor = self.history_treeview.get_cursor()[0] self.history_list.clear() - for item in self.wallet.get_tx_history(): + for item in self.wallet.get_history(): tx_hash, conf, is_mine, value, fee, balance, timestamp = item if conf > 0: try: @@ -1199,7 +1199,7 @@ class ElectrumWindow: import datetime if not tx_hash: return '' tx = self.wallet.transactions.get(tx_hash) - is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx) + is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash) if timestamp: diff --git a/gui/qt/lite_window.py b/gui/qt/lite_window.py @@ -450,7 +450,7 @@ class MiniWindow(QDialog): self.history_list.empty() for item in tx_history[-10:]: - tx_hash, conf, is_mine, value, fee, balance, timestamp = item + tx_hash, conf, value, timestamp = item label = self.actuator.g.wallet.get_label(tx_hash)[0] v_str = self.actuator.g.format_amount(value, True) self.history_list.append(label, v_str, age(timestamp)) @@ -862,7 +862,7 @@ class MiniDriver(QObject): self.window.update_completions(completions) def update_history(self): - tx_history = self.g.wallet.get_tx_history() + tx_history = self.g.wallet.get_history() self.window.update_history(tx_history) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -568,7 +568,7 @@ class ElectrumWindow(QMainWindow): for i,width in enumerate(self.column_widths['history']): l.setColumnWidth(i, width) l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] ) - l.itemDoubleClicked.connect(self.tx_label_clicked) + l.itemDoubleClicked.connect(self.edit_tx_label) l.itemChanged.connect(self.tx_label_changed) l.customContextMenuRequested.connect(self.create_history_menu) return l @@ -593,7 +593,7 @@ class ElectrumWindow(QMainWindow): menu = QMenu() menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash)) menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash))) - menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2)) + menu.addAction(_("Edit description"), lambda: self.edit_tx_label(item,2)) menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash)) menu.exec_(self.contacts_list.viewport().mapToGlobal(position)) @@ -603,21 +603,25 @@ class ElectrumWindow(QMainWindow): d = transaction_dialog.TxDialog(tx, self) d.exec_() - def tx_label_clicked(self, item, column): + def edit_tx_label(self, item, column): if column==2 and item.isSelected(): - self.is_edit=True + text = unicode(item.text(column)) + tx_hash = str(item.data(0, Qt.UserRole).toString()) + self.is_edit = True + if text == self.wallet.get_default_label(tx_hash): + item.setText(column, '') item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) self.history_list.editItem( item, column ) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) - self.is_edit=False + self.is_edit = False def tx_label_changed(self, item, column): if self.is_edit: return - self.is_edit=True + self.is_edit = True tx_hash = str(item.data(0, Qt.UserRole).toString()) tx = self.wallet.transactions.get(tx_hash) - text = unicode( item.text(2) ) + text = unicode(item.text(2)) self.wallet.set_label(tx_hash, text) if text: item.setForeground(2, QBrush(QColor('black'))) @@ -625,7 +629,7 @@ class ElectrumWindow(QMainWindow): text = self.wallet.get_default_label(tx_hash) item.setText(2, text) item.setForeground(2, QBrush(QColor('gray'))) - self.is_edit=False + self.is_edit = False def edit_label(self, is_recv): @@ -682,8 +686,9 @@ class ElectrumWindow(QMainWindow): def update_history_tab(self): self.history_list.clear() - for item in self.wallet.get_tx_history(self.current_account): - tx_hash, conf, is_mine, value, fee, balance, timestamp = item + balance = 0 + for item in self.wallet.get_history(self.current_account): + tx_hash, conf, value, timestamp = item time_str = _("unknown") if conf > 0: time_str = self.format_time(timestamp) @@ -703,6 +708,7 @@ class ElectrumWindow(QMainWindow): else: v_str = '--' + balance += value balance_str = self.format_amount(balance, whitespaces=True) if tx_hash: @@ -721,7 +727,7 @@ class ElectrumWindow(QMainWindow): item.setData(0, Qt.UserRole, tx_hash) item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) ) if is_default_label: - item.setForeground(2, QBrush(QColor('grey'))) + item.setForeground(2, QBrush(QColor('lightgrey'))) item.setIcon(0, icon) self.history_list.insertTopLevelItem(0,item) @@ -1020,7 +1026,7 @@ class ElectrumWindow(QMainWindow): for i in inputs: self.wallet.add_input_info(i) addr = self.payto_e.payto_address if self.payto_e.payto_address else self.dummy_address output = ('address', addr, sendable) - dummy_tx = Transaction(inputs, [output]) + dummy_tx = Transaction.from_io(inputs, [output]) fee = self.wallet.estimated_fee(dummy_tx) self.amount_e.setAmount(max(0,sendable-fee)) self.amount_e.textEdited.emit("") @@ -2510,10 +2516,10 @@ class ElectrumWindow(QMainWindow): def do_export_history(self, wallet, fileName, is_csv): - history = wallet.get_tx_history() + history = wallet.get_history() lines = [] for item in history: - tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item + tx_hash, confirmations, value, timestamp = item if confirmations: if timestamp is not None: try: @@ -2531,27 +2537,21 @@ class ElectrumWindow(QMainWindow): else: value_string = '--' - if fee is not None: - fee_string = format_satoshis(fee, True) - else: - fee_string = '0' - if tx_hash: label, is_default_label = wallet.get_label(tx_hash) label = label.encode('utf-8') else: label = "" - balance_string = format_satoshis(balance, False) if is_csv: - lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string]) + lines.append([tx_hash, label, confirmations, value_string, time_string]) else: lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string}) with open(fileName, "w+") as f: if is_csv: transaction = csv.writer(f, lineterminator='\n') - transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"]) + transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"]) for line in lines: transaction.writerow(line) else: diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py @@ -134,8 +134,8 @@ class TxDialog(QDialog): def update(self): - - is_relevant, is_mine, v, fee = self.wallet.get_tx_value(self.tx) + is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(self.tx) + tx_hash = self.tx.hash() if self.wallet.can_sign(self.tx): self.sign_button.show() else: @@ -143,7 +143,6 @@ class TxDialog(QDialog): if self.tx.is_complete(): status = _("Signed") - tx_hash = self.tx.hash() if tx_hash in self.wallet.transactions.keys(): conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash) @@ -182,10 +181,10 @@ class TxDialog(QDialog): if is_relevant: if is_mine: if fee is not None: - self.amount_label.setText(_("Amount sent:")+' %s'% self.parent.format_amount(v-fee) + ' ' + self.parent.base_unit()) - self.fee_label.setText(_("Transaction fee")+': %s'% self.parent.format_amount(fee) + ' ' + self.parent.base_unit()) + self.amount_label.setText(_("Amount sent:")+' %s'% self.parent.format_amount(-v+fee) + ' ' + self.parent.base_unit()) + self.fee_label.setText(_("Transaction fee")+': %s'% self.parent.format_amount(-fee) + ' ' + self.parent.base_unit()) else: - self.amount_label.setText(_("Amount sent:")+' %s'% self.parent.format_amount(v) + ' ' + self.parent.base_unit()) + self.amount_label.setText(_("Amount sent:")+' %s'% self.parent.format_amount(-v) + ' ' + self.parent.base_unit()) self.fee_label.setText(_("Transaction fee")+': '+ _("unknown")) else: self.amount_label.setText(_("Amount received:")+' %s'% self.parent.format_amount(v) + ' ' + self.parent.base_unit()) diff --git a/gui/text.py b/gui/text.py @@ -107,7 +107,7 @@ class ElectrumGui: b = 0 self.history = [] - for item in self.wallet.get_tx_history(): + for item in self.wallet.get_history(): tx_hash, conf, is_mine, value, fee, balance, timestamp = item if conf: try: diff --git a/lib/commands.py b/lib/commands.py @@ -342,7 +342,7 @@ class Commands: def history(self): balance = 0 out = [] - for item in self.wallet.get_tx_history(): + for item in self.wallet.get_history(): tx_hash, conf, is_mine, value, fee, balance, timestamp = item try: time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3] diff --git a/lib/synchronizer.py b/lib/synchronizer.py @@ -101,6 +101,7 @@ class WalletSynchronizer(util.DaemonThread): if not self.wallet.is_up_to_date(): self.wallet.set_up_to_date(True) self.was_updated = True + self.wallet.save_transactions() else: if self.wallet.is_up_to_date(): self.wallet.set_up_to_date(False) @@ -127,7 +128,7 @@ class WalletSynchronizer(util.DaemonThread): if method == 'blockchain.address.subscribe': addr = params[0] - if self.wallet.get_status(self.wallet.get_history(addr)) != result: + if self.wallet.get_status(self.wallet.get_address_history(addr)) != result: if requested_histories.get(addr) is None: self.network.send([('blockchain.address.get_history', [addr])], self.queue.put) requested_histories[addr] = result @@ -135,41 +136,43 @@ class WalletSynchronizer(util.DaemonThread): elif method == 'blockchain.address.get_history': addr = params[0] self.print_error("receiving history", addr, result) - if result == ['*']: - assert requested_histories.pop(addr) == '*' - self.wallet.receive_history_callback(addr, result) - else: - hist = [] - # check that txids are unique - txids = [] - for item in result: - tx_hash = item['tx_hash'] - if tx_hash not in txids: - txids.append(tx_hash) - hist.append( (tx_hash, item['height']) ) - - if len(hist) != len(result): - raise Exception("error: server sent history with non-unique txid", result) - - # check that the status corresponds to what was announced - rs = requested_histories.pop(addr) - if self.wallet.get_status(hist) != rs: - raise Exception("error: status mismatch: %s"%addr) - - # store received history - self.wallet.receive_history_callback(addr, hist) - - # request transactions that we don't have - for tx_hash, tx_height in hist: - if self.wallet.transactions.get(tx_hash) is None: - if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx: - missing_tx.append( (tx_hash, tx_height) ) + hist = [] + # check that txids are unique + txids = [] + for item in result: + tx_hash = item['tx_hash'] + if tx_hash not in txids: + txids.append(tx_hash) + hist.append( (tx_hash, item['height']) ) + + if len(hist) != len(result): + raise Exception("error: server sent history with non-unique txid", result) + + # check that the status corresponds to what was announced + rs = requested_histories.pop(addr) + if self.wallet.get_status(hist) != rs: + raise Exception("error: status mismatch: %s"%addr) + + # store received history + self.wallet.receive_history_callback(addr, hist) + + # request transactions that we don't have + for tx_hash, tx_height in hist: + if self.wallet.transactions.get(tx_hash) is None: + if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx: + missing_tx.append( (tx_hash, tx_height) ) elif method == 'blockchain.transaction.get': tx_hash = params[0] tx_height = params[1] assert tx_hash == bitcoin.hash_encode(bitcoin.Hash(result.decode('hex'))) - tx = Transaction.deserialize(result) + tx = Transaction(result) + try: + tx.deserialize() + except Exception: + self.print_msg("Warning: Cannot deserialize transactions. skipping") + continue + self.wallet.receive_tx_callback(tx_hash, tx, tx_height) self.was_updated = True requested_tx.remove( (tx_hash, tx_height) ) diff --git a/lib/transaction.py b/lib/transaction.py @@ -482,26 +482,25 @@ class Transaction: self.raw = self.serialize() return self.raw - def __init__(self, inputs, outputs, locktime=0): - self.inputs = inputs - self.outputs = outputs - self.locktime = locktime - self.raw = None - - @classmethod - def deserialize(klass, raw): - self = klass([],[]) - self.update(raw) - return self - - def update(self, raw): - d = deserialize(raw) + def __init__(self, raw): self.raw = raw + + def deserialize(self): + d = deserialize(self.raw) self.inputs = d['inputs'] self.outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']] self.locktime = d['lockTime'] @classmethod + def from_io(klass, inputs, outputs, locktime=0): + self = klass(None) + self.inputs = inputs + self.outputs = outputs + self.locktime = locktime + #self.raw = self.serialize() + return self + + @classmethod def sweep(klass, privkeys, network, to_address, fee): inputs = [] for privkey in privkeys: @@ -526,7 +525,7 @@ class Transaction: total = sum(i.get('value') for i in inputs) - fee outputs = [('address', to_address, total)] - self = klass(inputs, outputs) + self = klass.from_io(inputs, outputs) self.sign({ pubkey:privkey }) return self @@ -736,16 +735,6 @@ class Transaction: self.raw = self.serialize() - def add_pubkey_addresses(self, txdict): - for txin in self.inputs: - if txin.get('address') == "(pubkey)": - prev_tx = txdict.get(txin.get('prevout_hash')) - if prev_tx: - address, value = prev_tx.get_outputs()[txin.get('prevout_n')] - print_error("found pay-to-pubkey address:", address) - txin["address"] = address - - def get_outputs(self): """convert pubkeys to addresses""" o = [] @@ -767,60 +756,8 @@ class Transaction: return (addr in self.get_output_addresses()) or (addr in (tx.get("address") for tx in self.inputs)) - def get_value(self, addresses, prevout_values): - # return the balance for that tx - is_relevant = False - is_send = False - is_pruned = False - is_partial = False - v_in = v_out = v_out_mine = 0 - - for item in self.inputs: - addr = item.get('address') - if addr in addresses: - is_send = True - is_relevant = True - key = item['prevout_hash'] + ':%d'%item['prevout_n'] - value = prevout_values.get( key ) - if value is None: - is_pruned = True - else: - v_in += value - else: - is_partial = True - - if not is_send: is_partial = False - - for addr, value in self.get_outputs(): - v_out += value - if addr in addresses: - v_out_mine += value - is_relevant = True - - if is_pruned: - # some inputs are mine: - fee = None - if is_send: - v = v_out_mine - v_out - else: - # no input is mine - v = v_out_mine - - else: - v = v_out_mine - v_in - - if is_partial: - # some inputs are mine, but not all - fee = None - is_send = v < 0 - else: - # all inputs are mine - fee = v_out - v_in - - return is_relevant, is_send, v, fee - - def as_dict(self): + self.deserialize() import json out = { "hex":str(self), diff --git a/lib/wallet.py b/lib/wallet.py @@ -28,6 +28,7 @@ import json import copy from util import print_msg, print_error, NotEnoughFunds +from util import profiler from bitcoin import * from account import * @@ -173,9 +174,6 @@ class Abstract_Wallet(object): self.load_transactions() - # not saved - self.prevout_values = {} # my own transaction outputs - self.spent_outputs = [] # spv self.verifier = None # there is a difference between wallet.up_to_date and interface.is_up_to_date() @@ -185,42 +183,35 @@ class Abstract_Wallet(object): self.lock = threading.Lock() self.transaction_lock = threading.Lock() self.tx_event = threading.Event() - for tx_hash, tx in self.transactions.items(): - self.update_tx_outputs(tx_hash) # save wallet type the first time if self.storage.get('wallet_type') is None: self.storage.put('wallet_type', self.wallet_type, True) - + @profiler def load_transactions(self): + self.txi = self.storage.get('txi', {}) + self.txo = self.storage.get('txo', {}) + self.reverse_txo = self.storage.get('reverse_utxo', {}) + tx_list = self.storage.get('transactions', {}) self.transactions = {} - tx_list = self.storage.get('transactions',{}) - for k, raw in tx_list.items(): - try: - tx = Transaction.deserialize(raw) - except Exception: - print_msg("Warning: Cannot deserialize transactions. skipping") - continue - self.add_pubkey_addresses(tx) - self.transactions[k] = tx - for h,tx in self.transactions.items(): - if not self.check_new_tx(h, tx): - print_error("removing unreferenced tx", h) - self.transactions.pop(h) - - def add_pubkey_addresses(self, tx): - # find the address corresponding to pay-to-pubkey inputs - h = tx.hash() - - # inputs - tx.add_pubkey_addresses(self.transactions) - - # outputs of tx: inputs of tx2 - for type, x, v in tx.outputs: - if type == 'pubkey': - for tx2 in self.transactions.values(): - tx2.add_pubkey_addresses({h:tx}) + for tx_hash, raw in tx_list.items(): + tx = Transaction(raw) + self.transactions[tx_hash] = tx + if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None: + print_error("removing unreferenced tx", tx_hash) + self.transactions.pop(tx_hash) + + @profiler + def save_transactions(self): + with self.transaction_lock: + tx = {} + for k,v in self.transactions.items(): + tx[k] = str(v) + self.storage.put('transactions', tx) + self.storage.put('txi', self.txi) + self.storage.put('txo', self.txo) + self.storage.put('reverse_txo', self.reverse_txo) def get_action(self): pass @@ -362,7 +353,6 @@ class Abstract_Wallet(object): account_id, sequence = self.get_address_index(address) return self.accounts[account_id].get_pubkeys(*sequence) - def sign_message(self, address, message, password): keys = self.get_private_key(address, password) assert len(keys) == 1 @@ -396,7 +386,7 @@ class Abstract_Wallet(object): def fill_addressbook(self): # todo: optimize this for tx_hash, tx in self.transactions.viewitems(): - is_relevant, is_send, _, _ = self.get_tx_value(tx) + _, is_send, _, _ = self.get_tx_value(tx) if is_send: for addr in tx.get_output_addresses(): if not self.is_mine(addr) and addr not in self.addressbook: @@ -405,66 +395,139 @@ class Abstract_Wallet(object): # self.update_tx_labels() def get_num_tx(self, address): - n = 0 - for tx in self.transactions.values(): - if address in tx.get_output_addresses(): n += 1 - return n - - def get_tx_value(self, tx, account=None): - domain = self.get_account_addresses(account) - return tx.get_value(domain, self.prevout_values) - - def update_tx_outputs(self, tx_hash): - tx = self.transactions.get(tx_hash) - - for i, (addr, value) in enumerate(tx.get_outputs()): - key = tx_hash+ ':%d'%i - self.prevout_values[key] = value - + """ return number of transactions where address is involved """ + return len(self.history.get(address, [])) + #n = 0 + #for tx in self.transactions.values(): + # if address in tx.get_output_addresses(): n += 1 + #return n + + def get_tx_delta(self, tx_hash, address): + "effect of tx on address" + delta = 0 + # substract the value of coins sent from address + d = self.txi.get(tx_hash, {}).get(address, []) + for n, v in d: + delta -= v + # add the value of the coins received at address + d = self.txo.get(tx_hash, {}).get(address, []) + for n, v, cb in d: + delta += v + return delta + + def get_wallet_delta(self, tx): + """ effect of tx on wallet """ + addresses = self.addresses(True) + is_relevant = False + is_send = False + is_pruned = False + is_partial = False + v_in = v_out = v_out_mine = 0 for item in tx.inputs: - if self.is_mine(item.get('address')): - key = item['prevout_hash'] + ':%d'%item['prevout_n'] - self.spent_outputs.append(key) + addr = item.get('address') + if addr in addresses: + is_send = True + is_relevant = True + d = self.txo.get(item['prevout_hash'], {}).get(addr, []) + for n, v, cb in d: + if n == item['prevout_n']: + value = v + break + else: + value = None + if value is None: + is_pruned = True + else: + v_in += value + else: + is_partial = True + if not is_send: + is_partial = False + for addr, value in tx.get_outputs(): + v_out += value + if addr in addresses: + v_out_mine += value + is_relevant = True + if is_pruned: + # some inputs are mine: + fee = None + if is_send: + v = v_out_mine - v_out + else: + # no input is mine + v = v_out_mine + else: + v = v_out_mine - v_in + if is_partial: + # some inputs are mine, but not all + fee = None + is_send = v < 0 + else: + # all inputs are mine + fee = v_out - v_in + return is_relevant, is_send, v, fee def get_addr_balance(self, address): - 'returns the confirmed balance and pending (unconfirmed) balance change of this bitcoin address' - #assert self.is_mine(address) - h = self.history.get(address,[]) - if h == ['*']: return 0,0 + "returns the confirmed balance and pending (unconfirmed) balance change of a bitcoin address" + h = self.history.get(address, []) c = u = 0 - received_coins = [] # list of coins received at address - # go through all tx in history of this address and collect the coins arriving on this address - for tx_hash, tx_height in h: - tx = self.transactions.get(tx_hash) - if not tx: continue + for tx_hash, height in h: + v = self.get_tx_delta(tx_hash, address) + if height > 0: + c += v + else: + u += v + return c, u - for i, (addr, value) in enumerate(tx.get_outputs()): - if addr == address: - key = tx_hash + ':%d'%i - received_coins.append(key) - # go through all tx in history of this address again - for tx_hash, tx_height in h: - tx = self.transactions.get(tx_hash) - if not tx: continue - v = 0 - # substract the value of coins leaving from this address - for item in tx.inputs: - addr = item.get('address') - if addr == address: - key = item['prevout_hash'] + ':%d'%item['prevout_n'] - value = self.prevout_values.get( key ) - if key in received_coins: - v -= value - # add the value of the coins arriving in this address - for i, (addr, value) in enumerate(tx.get_outputs()): - key = tx_hash + ':%d'%i - if addr == address: - v += value - - if tx_height: - c += v # confirmed coins value + def get_addr_utxo(self, address): + h = self.history.get(address, []) + coins = {} + for tx_hash, height in h: + l = self.txo.get(tx_hash, {}).get(address, []) + for n, v, is_cb in l: + coins[tx_hash + ':%d'%n] = (height, v, is_cb) + for tx_hash, height in h: + l = self.txi.get(tx_hash, {}).get(address, []) + for txi, v in l: + coins.pop(txi) + return coins.items() + + def get_unspent_coins(self, domain=None): + coins = [] + if domain is None: + domain = self.addresses(True) + for addr in domain: + c = self.get_addr_utxo(addr) + for txo, v in c: + tx_height, value, is_cb = v + prevout_hash, prevout_n = txo.split(':') + output = { + 'address':addr, + 'value':value, + 'prevout_n':int(prevout_n), + 'prevout_hash':prevout_hash, + 'height':tx_height, + 'coinbase':is_cb + } + coins.append((tx_height, output)) + continue + # sort by age + if coins: + coins = sorted(coins) + if coins[-1][0] != 0: + while coins[0][0] == 0: + coins = coins[1:] + [ coins[0] ] + return [value for height, value in coins] + + def get_addr_balance2(self, address): + "returns the confirmed balance and pending (unconfirmed) balance change of a bitcoin address" + coins = self.get_addr_utxo(address) + c = u = 0 + for txo, v, height in coins: + if height > 0: + c += v else: - u += v # unconfirmed coins value + u += v return c, u def get_account_name(self, k): @@ -508,133 +571,170 @@ class Abstract_Wallet(object): uu += u return cc, uu - def get_unspent_coins(self, domain=None): - coins = [] - if domain is None: domain = self.addresses(True) - for addr in domain: - h = self.history.get(addr, []) - if h == ['*']: continue - for tx_hash, tx_height in h: - tx = self.transactions.get(tx_hash) - if tx is None: raise Exception("Wallet not synchronized") - is_coinbase = tx.inputs[0].get('prevout_hash') == '0'*64 - for i, (address, value) in enumerate(tx.get_outputs()): - output = {'address':address, 'value':value, 'prevout_n':i} - if address != addr: continue - key = tx_hash + ":%d"%i - if key in self.spent_outputs: continue - output['prevout_hash'] = tx_hash - output['height'] = tx_height - output['coinbase'] = is_coinbase - coins.append((tx_height, output)) - - # sort by age - if coins: - coins = sorted(coins) - if coins[-1][0] != 0: - while coins[0][0] == 0: - coins = coins[1:] + [ coins[0] ] - return [value for height, value in coins] - - - def set_fee(self, fee): if self.fee_per_kb != fee: self.fee_per_kb = fee self.storage.put('fee_per_kb', self.fee_per_kb, True) - - def get_history(self, address): + def get_address_history(self, address): with self.lock: - return self.history.get(address) + return self.history.get(address, []) def get_status(self, h): - if not h: return None - if h == ['*']: return '*' + if not h: + return None status = '' for tx_hash, height in h: status += tx_hash + ':%d:' % height return hashlib.sha256( status ).digest().encode('hex') - def receive_tx_callback(self, tx_hash, tx, tx_height): - + def add_transaction(self, tx_hash, tx, tx_height): + is_coinbase = tx.inputs[0].get('prevout_hash') == '0'*64 with self.transaction_lock: - self.add_pubkey_addresses(tx) - if not self.check_new_tx(tx_hash, tx): - # may happen due to pruning - print_error("received transaction that is no longer referenced in history", tx_hash) - return + # add inputs + self.txi[tx_hash] = d = {} + for txi in tx.inputs: + addr = txi.get('address') + if addr and self.is_mine(addr): + prevout_hash = txi['prevout_hash'] + prevout_n = txi['prevout_n'] + ser = prevout_hash + ':%d'%prevout_n + dd = self.txo.get(prevout_hash, {}) + for n, v, is_cb in dd.get(addr, []): + if n == prevout_n: + if d.get(addr) is None: + d[addr] = [] + d[addr].append((ser, v)) + break + else: + self.reverse_txo[ser] = tx_hash + elif addr == "(pubkey)": + prevout_hash = txi['prevout_hash'] + prevout_n = txi['prevout_n'] + ser = prevout_hash + ':%d'%prevout_n + dd = self.txo.get(prevout_hash, {}) + found = False + for _addr, l in dd.items(): + for n, v, is_cb in l: + if n == prevout_n: + print_error("found pay-to-pubkey address:", _addr) + if d.get(_addr) is None: + d[_addr] = [] + d[_addr].append((ser, v)) + found = True + if not found: + self.reverse_txo[ser] = tx_hash + + # add outputs + self.txo[tx_hash] = d = {} + for n, txo in enumerate(tx.outputs): + ser = tx_hash + ':%d'%n + _type, x, v = txo + if _type == 'address': + addr = x + elif _type == 'pubkey': + addr = public_key_to_bc_address(x.decode('hex')) + else: + addr = None + if addr and self.is_mine(addr): + if d.get(addr) is None: + d[addr] = [] + d[addr].append((n, v, is_coinbase)) + + next_tx = self.reverse_txo.get(ser) + if next_tx is not None: + self.reverse_txo.pop(ser) + dd = self.txi.get(next_tx, {}) + if dd.get(addr) is None: + dd[addr] = [] + dd[addr].append((ser, v)) + # save self.transactions[tx_hash] = tx - self.network.pending_transactions_for_notifications.append(tx) - self.save_transactions() - if self.verifier and tx_height>0: - self.verifier.add(tx_hash, tx_height) - self.update_tx_outputs(tx_hash) - def save_transactions(self): - tx = {} - for k,v in self.transactions.items(): - tx[k] = str(v) - self.storage.put('transactions', tx, True) + def receive_tx_callback(self, tx_hash, tx, tx_height): + if not self.check_new_tx(tx_hash, tx): + # may happen due to pruning + print_error("received transaction that is no longer referenced in history", tx_hash) + return + self.add_transaction(tx_hash, tx, tx_height) + #self.network.pending_transactions_for_notifications.append(tx) + if self.verifier and tx_height>0: + self.verifier.add(tx_hash, tx_height) + def receive_history_callback(self, addr, hist): - if not self.check_new_history(addr, hist): - raise Exception("error: received history for %s is not consistent with known transactions"%addr) + #if not self.check_new_history(addr, hist): + # raise Exception("error: received history for %s is not consistent with known transactions"%addr) with self.lock: self.history[addr] = hist self.storage.put('addr_history', self.history, True) - if hist != ['*']: - for tx_hash, tx_height in hist: - if tx_height>0: - # add it in case it was previously unconfirmed - if self.verifier: self.verifier.add(tx_hash, tx_height) - - def get_tx_history(self, account=None): - if not self.verifier: - return [] - - with self.transaction_lock: - history = self.transactions.items() - history.sort(key = lambda x: self.verifier.get_txpos(x[0])) - result = [] + for tx_hash, tx_height in hist: + if tx_height>0: + # add it in case it was previously unconfirmed + if self.verifier: + self.verifier.add(tx_hash, tx_height) - balance = 0 - for tx_hash, tx in history: - is_relevant, is_mine, v, fee = self.get_tx_value(tx, account) - if v is not None: balance += v + # if addr is new, we have to recompute txi and txo + # fixme: bad interaction with server hist limit? + tx = self.transactions.get(tx_hash) + if tx is not None and self.txi.get(tx_hash, {}).get(addr) is None and self.txo.get(tx_hash, {}).get(addr) is None: + tx.deserialize() + self.add_transaction(tx_hash, tx, tx_height) - c, u = self.get_account_balance(account) - if balance != c+u: - result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) ) + def get_history(self, account=None): + # get domain + domain = self.get_account_addresses(account) - balance = c + u - balance - for tx_hash, tx in history: - is_relevant, is_mine, value, fee = self.get_tx_value(tx, account) - if not is_relevant: - continue - if value is not None: - balance += value + hh = [] + # 1. Get the history of each address in the domain + for addr in domain: + h = self.get_address_history(addr) + for tx_hash, height in h: + delta = self.get_tx_delta(tx_hash, addr) + hh.append([addr, tx_hash, height, delta]) + + # 2. merge + # the delta of a tx on the domain is the sum of its deltas on addresses + merged = {} + for addr, tx_hash, height, delta in hh: + if tx_hash not in merged: + merged[tx_hash] = (height, delta) + else: + h, d = merged.get(tx_hash) + merged[tx_hash] = (h, d + delta) + + # 3. create sorted list + history = [] + for tx_hash, v in merged.items(): + height, value = v + is_mine = 1 + fee = 0 + balance = 0 + conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None) + history.append( (tx_hash, conf, value, timestamp) ) - conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None) - result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) ) + history.sort(key = lambda x: self.verifier.get_txpos(x[0])) + return history - return result def get_label(self, tx_hash): label = self.labels.get(tx_hash) is_default = (label == '') or (label is None) - if is_default: label = self.get_default_label(tx_hash) + if is_default: + label = self.get_default_label(tx_hash) return label, is_default def get_default_label(self, tx_hash): + return tx_hash + tx = self.transactions.get(tx_hash) default_label = '' if tx: - is_relevant, is_mine, _, _ = self.get_tx_value(tx) + _, is_mine, _, _ = self.get_wallet_delta(tx) if is_mine: for o_addr in tx.get_output_addresses(): if not self.is_mine(o_addr): @@ -702,7 +802,7 @@ class Abstract_Wallet(object): amount = sum( map(lambda x:x[2], outputs) ) total = fee = 0 inputs = [] - tx = Transaction(inputs, outputs) + tx = Transaction.from_io(inputs, outputs) for item in coins: if item.get('coinbase') and item.get('height') + COINBASE_MATURITY > self.network.get_local_height(): continue @@ -860,7 +960,6 @@ class Abstract_Wallet(object): # review transactions that are in the history for addr, hist in self.history.items(): - if hist == ['*']: continue for tx_hash, tx_height in hist: if tx_height>0: # add it in case it was previously unconfirmed @@ -874,23 +973,22 @@ class Abstract_Wallet(object): def check_new_history(self, addr, hist): # check that all tx in hist are relevant - if hist != ['*']: - for tx_hash, height in hist: - tx = self.transactions.get(tx_hash) - if not tx: continue - if not tx.has_address(addr): - return False + for tx_hash, height in hist: + tx = self.transactions.get(tx_hash) + if not tx: + continue + if not tx.has_address(addr): + return False # check that we are not "orphaning" a transaction old_hist = self.history.get(addr,[]) - if old_hist == ['*']: return True - for tx_hash, height in old_hist: - if tx_hash in map(lambda x:x[0], hist): continue + if tx_hash in map(lambda x:x[0], hist): + continue found = False for _addr, _hist in self.history.items(): - if _addr == addr: continue - if _hist == ['*']: continue + if _addr == addr: + continue _tx_hist = map(lambda x:x[0], _hist) if tx_hash in _tx_hist: found = True @@ -916,7 +1014,6 @@ class Abstract_Wallet(object): print_error("sync:", ext_requests, ext_h) height = None for h in ext_h: - if h == ['*']: continue for item in h: if item.get('tx_hash') == tx_hash: height = item.get('height') @@ -933,7 +1030,6 @@ class Abstract_Wallet(object): # 1 check that tx is referenced in addr_history. addresses = [] for addr, hist in self.history.items(): - if hist == ['*']:continue for txh, height in hist: if txh == tx_hash: addresses.append(addr) @@ -996,8 +1092,6 @@ class Abstract_Wallet(object): def address_is_old(self, address, age_limit=2): age = -1 h = self.history.get(address, []) - if h == ['*']: - return True for tx_hash, tx_height in h: if tx_height == 0: tx_age = 0 diff --git a/plugins/exchange_rate.py b/plugins/exchange_rate.py @@ -499,9 +499,9 @@ class Plugin(BasePlugin): @hook def load_wallet(self, wallet): tx_list = {} - for item in self.wallet.get_tx_history(self.wallet.storage.get("current_account", None)): - tx_hash, conf, is_mine, value, fee, balance, timestamp = item - tx_list[tx_hash] = {'value': value, 'timestamp': timestamp, 'balance': balance} + for item in self.wallet.get_history(self.wallet.storage.get("current_account", None)): + tx_hash, conf, value, timestamp = item + tx_list[tx_hash] = {'value': value, 'timestamp': timestamp } self.tx_list = tx_list self.cur_exchange = self.config.get('use_exchange', "Blockchain") @@ -572,20 +572,21 @@ class Plugin(BasePlugin): except Exception: newtx = self.wallet.get_tx_history() v = newtx[[x[0] for x in newtx].index(str(item.data(0, Qt.UserRole).toPyObject()))][3] - tx_info = {'timestamp':int(time.time()), 'value': v } + tx_info = {'timestamp':int(time.time()), 'value': v} pass tx_time = int(tx_info['timestamp']) + tx_value = Decimal(str(tx_info['value'])) / 100000000 if self.cur_exchange == "CoinDesk": tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d') try: - tx_fiat_val = "%.2f %s" % (Decimal(str(tx_info['value'])) / 100000000 * Decimal(self.resp_hist['bpi'][tx_time_str]), "USD") + tx_fiat_val = "%.2f %s" % (value * Decimal(self.resp_hist['bpi'][tx_time_str]), "USD") except KeyError: tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(str(tx_info['value']))/100000000 , "USD") elif self.cur_exchange == "Winkdex": tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d') + "T16:00:00-04:00" try: tx_rate = self.resp_hist[[x['timestamp'] for x in self.resp_hist].index(tx_time_str)]['price'] - tx_fiat_val = "%.2f %s" % (Decimal(tx_info['value']) / 100000000 * Decimal(tx_rate)/Decimal("100.0"), "USD") + tx_fiat_val = "%.2f %s" % (tx_value * Decimal(tx_rate)/Decimal("100.0"), "USD") except ValueError: tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(tx_info['value'])/100000000 , "USD") except KeyError: @@ -594,7 +595,7 @@ class Plugin(BasePlugin): tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d') try: num = self.resp_hist[tx_time_str].replace(',','') - tx_fiat_val = "%.2f %s" % (Decimal(str(tx_info['value'])) / 100000000 * Decimal(num), self.fiat_unit()) + tx_fiat_val = "%.2f %s" % (tx_value * Decimal(num), self.fiat_unit()) except KeyError: tx_fiat_val = _("No data") diff --git a/plugins/plot.py b/plugins/plot.py @@ -42,30 +42,28 @@ class Plugin(BasePlugin): @hook def export_history_dialog(self, d,hbox): self.wallet = d.wallet - - history = self.wallet.get_tx_history() - + history = self.wallet.get_history() if len(history) > 0: b = QPushButton(_("Preview plot")) hbox.addWidget(b) - b.clicked.connect(lambda: self.do_plot(self.wallet)) + b.clicked.connect(lambda: self.do_plot(self.wallet, history)) else: b = QPushButton(_("No history to plot")) hbox.addWidget(b) - - def do_plot(self,wallet): - history = wallet.get_tx_history() + def do_plot(self, wallet, history): balance_Val=[] fee_val=[] value_val=[] datenums=[] - unknown_trans=0 - pending_trans=0 - counter_trans=0 + unknown_trans = 0 + pending_trans = 0 + counter_trans = 0 + balance = 0 for item in history: - tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item + tx_hash, confirmations, value, timestamp = item + balance += value if confirmations: if timestamp is not None: try: @@ -73,24 +71,15 @@ class Plugin(BasePlugin): balance_string = format_satoshis(balance, False) balance_Val.append(float((format_satoshis(balance,False)))*1000.0) except [RuntimeError, TypeError, NameError] as reason: - unknown_trans=unknown_trans+1 + unknown_trans += 1 pass else: - unknown_trans=unknown_trans+1 - else: - pending_trans=pending_trans+1 - - if value is not None: - value_string = format_satoshis(value, True) - value_val.append(float(value_string)*1000.0) + unknown_trans += 1 else: - value_string = '--' + pending_trans += 1 - if fee is not None: - fee_string = format_satoshis(fee, True) - fee_val.append(float(fee_string)) - else: - fee_string = '0' + value_string = format_satoshis(value, True) + value_val.append(float(value_string)*1000.0) if tx_hash: label, is_default_label = wallet.get_label(tx_hash) @@ -139,12 +128,9 @@ class Plugin(BasePlugin): xfmt = md.DateFormatter('%Y-%m-%d') ax.xaxis.set_major_formatter(xfmt) - axarr[1].plot(datenums,fee_val,marker='o',linestyle='-',color='red',label='Fee') axarr[1].plot(datenums,value_val,marker='o',linestyle='-',color='green',label='Value') - - axarr[1].legend(loc='upper left') # plt.annotate('unknown transaction = %d \n pending transactions = %d' %(unknown_trans,pending_trans),xy=(0.7,0.05),xycoords='axes fraction',size=12) plt.show()