Browse Source

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

283
ThomasV 10 years ago
parent
commit
e3de121be9
  1. 4
      gui/gtk.py
  2. 4
      gui/qt/lite_window.py
  3. 44
      gui/qt/main_window.py
  4. 11
      gui/qt/transaction_dialog.py
  5. 2
      gui/text.py
  6. 2
      lib/commands.py
  7. 15
      lib/synchronizer.py
  8. 93
      lib/transaction.py
  9. 454
      lib/wallet.py
  10. 15
      plugins/exchange_rate.py
  11. 38
      plugins/plot.py

4
gui/gtk.py

@ -1170,7 +1170,7 @@ class ElectrumWindow:
cursor = self.history_treeview.get_cursor()[0] cursor = self.history_treeview.get_cursor()[0]
self.history_list.clear() 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 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
if conf > 0: if conf > 0:
try: try:
@ -1199,7 +1199,7 @@ class ElectrumWindow:
import datetime import datetime
if not tx_hash: return '' if not tx_hash: return ''
tx = self.wallet.transactions.get(tx_hash) 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) conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
if timestamp: if timestamp:

4
gui/qt/lite_window.py

@ -450,7 +450,7 @@ class MiniWindow(QDialog):
self.history_list.empty() self.history_list.empty()
for item in tx_history[-10:]: 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] label = self.actuator.g.wallet.get_label(tx_hash)[0]
v_str = self.actuator.g.format_amount(value, True) v_str = self.actuator.g.format_amount(value, True)
self.history_list.append(label, v_str, age(timestamp)) self.history_list.append(label, v_str, age(timestamp))
@ -862,7 +862,7 @@ class MiniDriver(QObject):
self.window.update_completions(completions) self.window.update_completions(completions)
def update_history(self): 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) self.window.update_history(tx_history)

44
gui/qt/main_window.py

@ -568,7 +568,7 @@ class ElectrumWindow(QMainWindow):
for i,width in enumerate(self.column_widths['history']): for i,width in enumerate(self.column_widths['history']):
l.setColumnWidth(i, width) l.setColumnWidth(i, width)
l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] ) 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.itemChanged.connect(self.tx_label_changed)
l.customContextMenuRequested.connect(self.create_history_menu) l.customContextMenuRequested.connect(self.create_history_menu)
return l return l
@ -593,7 +593,7 @@ class ElectrumWindow(QMainWindow):
menu = QMenu() menu = QMenu()
menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash)) 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(_("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.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
menu.exec_(self.contacts_list.viewport().mapToGlobal(position)) menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
@ -603,21 +603,25 @@ class ElectrumWindow(QMainWindow):
d = transaction_dialog.TxDialog(tx, self) d = transaction_dialog.TxDialog(tx, self)
d.exec_() d.exec_()
def tx_label_clicked(self, item, column): def edit_tx_label(self, item, column):
if column==2 and item.isSelected(): 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) item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
self.history_list.editItem( item, column ) self.history_list.editItem( item, column )
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) 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): def tx_label_changed(self, item, column):
if self.is_edit: if self.is_edit:
return return
self.is_edit=True self.is_edit = True
tx_hash = str(item.data(0, Qt.UserRole).toString()) tx_hash = str(item.data(0, Qt.UserRole).toString())
tx = self.wallet.transactions.get(tx_hash) tx = self.wallet.transactions.get(tx_hash)
text = unicode( item.text(2) ) text = unicode(item.text(2))
self.wallet.set_label(tx_hash, text) self.wallet.set_label(tx_hash, text)
if text: if text:
item.setForeground(2, QBrush(QColor('black'))) item.setForeground(2, QBrush(QColor('black')))
@ -625,7 +629,7 @@ class ElectrumWindow(QMainWindow):
text = self.wallet.get_default_label(tx_hash) text = self.wallet.get_default_label(tx_hash)
item.setText(2, text) item.setText(2, text)
item.setForeground(2, QBrush(QColor('gray'))) item.setForeground(2, QBrush(QColor('gray')))
self.is_edit=False self.is_edit = False
def edit_label(self, is_recv): def edit_label(self, is_recv):
@ -682,8 +686,9 @@ class ElectrumWindow(QMainWindow):
def update_history_tab(self): def update_history_tab(self):
self.history_list.clear() self.history_list.clear()
for item in self.wallet.get_tx_history(self.current_account): balance = 0
tx_hash, conf, is_mine, value, fee, balance, timestamp = item for item in self.wallet.get_history(self.current_account):
tx_hash, conf, value, timestamp = item
time_str = _("unknown") time_str = _("unknown")
if conf > 0: if conf > 0:
time_str = self.format_time(timestamp) time_str = self.format_time(timestamp)
@ -703,6 +708,7 @@ class ElectrumWindow(QMainWindow):
else: else:
v_str = '--' v_str = '--'
balance += value
balance_str = self.format_amount(balance, whitespaces=True) balance_str = self.format_amount(balance, whitespaces=True)
if tx_hash: if tx_hash:
@ -721,7 +727,7 @@ class ElectrumWindow(QMainWindow):
item.setData(0, Qt.UserRole, tx_hash) item.setData(0, Qt.UserRole, tx_hash)
item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) ) item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
if is_default_label: if is_default_label:
item.setForeground(2, QBrush(QColor('grey'))) item.setForeground(2, QBrush(QColor('lightgrey')))
item.setIcon(0, icon) item.setIcon(0, icon)
self.history_list.insertTopLevelItem(0,item) self.history_list.insertTopLevelItem(0,item)
@ -1020,7 +1026,7 @@ class ElectrumWindow(QMainWindow):
for i in inputs: self.wallet.add_input_info(i) 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 addr = self.payto_e.payto_address if self.payto_e.payto_address else self.dummy_address
output = ('address', addr, sendable) output = ('address', addr, sendable)
dummy_tx = Transaction(inputs, [output]) dummy_tx = Transaction.from_io(inputs, [output])
fee = self.wallet.estimated_fee(dummy_tx) fee = self.wallet.estimated_fee(dummy_tx)
self.amount_e.setAmount(max(0,sendable-fee)) self.amount_e.setAmount(max(0,sendable-fee))
self.amount_e.textEdited.emit("") self.amount_e.textEdited.emit("")
@ -2510,10 +2516,10 @@ class ElectrumWindow(QMainWindow):
def do_export_history(self, wallet, fileName, is_csv): def do_export_history(self, wallet, fileName, is_csv):
history = wallet.get_tx_history() history = wallet.get_history()
lines = [] lines = []
for item in history: for item in history:
tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item tx_hash, confirmations, value, timestamp = item
if confirmations: if confirmations:
if timestamp is not None: if timestamp is not None:
try: try:
@ -2531,27 +2537,21 @@ class ElectrumWindow(QMainWindow):
else: else:
value_string = '--' value_string = '--'
if fee is not None:
fee_string = format_satoshis(fee, True)
else:
fee_string = '0'
if tx_hash: if tx_hash:
label, is_default_label = wallet.get_label(tx_hash) label, is_default_label = wallet.get_label(tx_hash)
label = label.encode('utf-8') label = label.encode('utf-8')
else: else:
label = "" label = ""
balance_string = format_satoshis(balance, False)
if is_csv: 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: else:
lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string}) lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
with open(fileName, "w+") as f: with open(fileName, "w+") as f:
if is_csv: if is_csv:
transaction = csv.writer(f, lineterminator='\n') 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: for line in lines:
transaction.writerow(line) transaction.writerow(line)
else: else:

11
gui/qt/transaction_dialog.py

@ -134,8 +134,8 @@ class TxDialog(QDialog):
def update(self): def update(self):
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(self.tx)
is_relevant, is_mine, v, fee = self.wallet.get_tx_value(self.tx) tx_hash = self.tx.hash()
if self.wallet.can_sign(self.tx): if self.wallet.can_sign(self.tx):
self.sign_button.show() self.sign_button.show()
else: else:
@ -143,7 +143,6 @@ class TxDialog(QDialog):
if self.tx.is_complete(): if self.tx.is_complete():
status = _("Signed") status = _("Signed")
tx_hash = self.tx.hash()
if tx_hash in self.wallet.transactions.keys(): if tx_hash in self.wallet.transactions.keys():
conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash) conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
@ -182,10 +181,10 @@ class TxDialog(QDialog):
if is_relevant: if is_relevant:
if is_mine: if is_mine:
if fee is not None: if fee is not None:
self.amount_label.setText(_("Amount sent:")+' %s'% self.parent.format_amount(v-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()) self.fee_label.setText(_("Transaction fee")+': %s'% self.parent.format_amount(-fee) + ' ' + self.parent.base_unit())
else: 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")) self.fee_label.setText(_("Transaction fee")+': '+ _("unknown"))
else: else:
self.amount_label.setText(_("Amount received:")+' %s'% self.parent.format_amount(v) + ' ' + self.parent.base_unit()) self.amount_label.setText(_("Amount received:")+' %s'% self.parent.format_amount(v) + ' ' + self.parent.base_unit())

2
gui/text.py

@ -107,7 +107,7 @@ class ElectrumGui:
b = 0 b = 0
self.history = [] 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 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
if conf: if conf:
try: try:

2
lib/commands.py

@ -342,7 +342,7 @@ class Commands:
def history(self): def history(self):
balance = 0 balance = 0
out = [] 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 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
try: try:
time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3] time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]

15
lib/synchronizer.py

@ -101,6 +101,7 @@ class WalletSynchronizer(util.DaemonThread):
if not self.wallet.is_up_to_date(): if not self.wallet.is_up_to_date():
self.wallet.set_up_to_date(True) self.wallet.set_up_to_date(True)
self.was_updated = True self.was_updated = True
self.wallet.save_transactions()
else: else:
if self.wallet.is_up_to_date(): if self.wallet.is_up_to_date():
self.wallet.set_up_to_date(False) self.wallet.set_up_to_date(False)
@ -127,7 +128,7 @@ class WalletSynchronizer(util.DaemonThread):
if method == 'blockchain.address.subscribe': if method == 'blockchain.address.subscribe':
addr = params[0] 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: if requested_histories.get(addr) is None:
self.network.send([('blockchain.address.get_history', [addr])], self.queue.put) self.network.send([('blockchain.address.get_history', [addr])], self.queue.put)
requested_histories[addr] = result requested_histories[addr] = result
@ -135,10 +136,6 @@ class WalletSynchronizer(util.DaemonThread):
elif method == 'blockchain.address.get_history': elif method == 'blockchain.address.get_history':
addr = params[0] addr = params[0]
self.print_error("receiving history", addr, result) self.print_error("receiving history", addr, result)
if result == ['*']:
assert requested_histories.pop(addr) == '*'
self.wallet.receive_history_callback(addr, result)
else:
hist = [] hist = []
# check that txids are unique # check that txids are unique
txids = [] txids = []
@ -169,7 +166,13 @@ class WalletSynchronizer(util.DaemonThread):
tx_hash = params[0] tx_hash = params[0]
tx_height = params[1] tx_height = params[1]
assert tx_hash == bitcoin.hash_encode(bitcoin.Hash(result.decode('hex'))) 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.wallet.receive_tx_callback(tx_hash, tx, tx_height)
self.was_updated = True self.was_updated = True
requested_tx.remove( (tx_hash, tx_height) ) requested_tx.remove( (tx_hash, tx_height) )

93
lib/transaction.py

@ -482,25 +482,24 @@ class Transaction:
self.raw = self.serialize() self.raw = self.serialize()
return self.raw return self.raw
def __init__(self, inputs, outputs, locktime=0): def __init__(self, raw):
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)
self.raw = raw self.raw = raw
def deserialize(self):
d = deserialize(self.raw)
self.inputs = d['inputs'] self.inputs = d['inputs']
self.outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']] self.outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']]
self.locktime = d['lockTime'] 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 @classmethod
def sweep(klass, privkeys, network, to_address, fee): def sweep(klass, privkeys, network, to_address, fee):
inputs = [] inputs = []
@ -526,7 +525,7 @@ class Transaction:
total = sum(i.get('value') for i in inputs) - fee total = sum(i.get('value') for i in inputs) - fee
outputs = [('address', to_address, total)] outputs = [('address', to_address, total)]
self = klass(inputs, outputs) self = klass.from_io(inputs, outputs)
self.sign({ pubkey:privkey }) self.sign({ pubkey:privkey })
return self return self
@ -736,16 +735,6 @@ class Transaction:
self.raw = self.serialize() 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): def get_outputs(self):
"""convert pubkeys to addresses""" """convert pubkeys to addresses"""
o = [] 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)) 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): def as_dict(self):
self.deserialize()
import json import json
out = { out = {
"hex":str(self), "hex":str(self),

454
lib/wallet.py

@ -28,6 +28,7 @@ import json
import copy import copy
from util import print_msg, print_error, NotEnoughFunds from util import print_msg, print_error, NotEnoughFunds
from util import profiler
from bitcoin import * from bitcoin import *
from account import * from account import *
@ -173,9 +174,6 @@ class Abstract_Wallet(object):
self.load_transactions() self.load_transactions()
# not saved
self.prevout_values = {} # my own transaction outputs
self.spent_outputs = []
# spv # spv
self.verifier = None self.verifier = None
# there is a difference between wallet.up_to_date and interface.is_up_to_date() # 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.lock = threading.Lock()
self.transaction_lock = threading.Lock() self.transaction_lock = threading.Lock()
self.tx_event = threading.Event() 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 # save wallet type the first time
if self.storage.get('wallet_type') is None: if self.storage.get('wallet_type') is None:
self.storage.put('wallet_type', self.wallet_type, True) self.storage.put('wallet_type', self.wallet_type, True)
@profiler
def load_transactions(self): 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 = {} self.transactions = {}
tx_list = self.storage.get('transactions',{}) for tx_hash, raw in tx_list.items():
for k, raw in tx_list.items(): tx = Transaction(raw)
try: self.transactions[tx_hash] = tx
tx = Transaction.deserialize(raw) if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None:
except Exception: print_error("removing unreferenced tx", tx_hash)
print_msg("Warning: Cannot deserialize transactions. skipping") self.transactions.pop(tx_hash)
continue
self.add_pubkey_addresses(tx) @profiler
self.transactions[k] = tx def save_transactions(self):
for h,tx in self.transactions.items(): with self.transaction_lock:
if not self.check_new_tx(h, tx): tx = {}
print_error("removing unreferenced tx", h) for k,v in self.transactions.items():
self.transactions.pop(h) tx[k] = str(v)
self.storage.put('transactions', tx)
def add_pubkey_addresses(self, tx): self.storage.put('txi', self.txi)
# find the address corresponding to pay-to-pubkey inputs self.storage.put('txo', self.txo)
h = tx.hash() self.storage.put('reverse_txo', self.reverse_txo)
# 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})
def get_action(self): def get_action(self):
pass pass
@ -362,7 +353,6 @@ class Abstract_Wallet(object):
account_id, sequence = self.get_address_index(address) account_id, sequence = self.get_address_index(address)
return self.accounts[account_id].get_pubkeys(*sequence) return self.accounts[account_id].get_pubkeys(*sequence)
def sign_message(self, address, message, password): def sign_message(self, address, message, password):
keys = self.get_private_key(address, password) keys = self.get_private_key(address, password)
assert len(keys) == 1 assert len(keys) == 1
@ -396,7 +386,7 @@ class Abstract_Wallet(object):
def fill_addressbook(self): def fill_addressbook(self):
# todo: optimize this # todo: optimize this
for tx_hash, tx in self.transactions.viewitems(): 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: if is_send:
for addr in tx.get_output_addresses(): for addr in tx.get_output_addresses():
if not self.is_mine(addr) and addr not in self.addressbook: if not self.is_mine(addr) and addr not in self.addressbook:
@ -405,66 +395,139 @@ class Abstract_Wallet(object):
# self.update_tx_labels() # self.update_tx_labels()
def get_num_tx(self, address): def get_num_tx(self, address):
n = 0 """ return number of transactions where address is involved """
for tx in self.transactions.values(): return len(self.history.get(address, []))
if address in tx.get_output_addresses(): n += 1 #n = 0
return n #for tx in self.transactions.values():
# if address in tx.get_output_addresses(): n += 1
def get_tx_value(self, tx, account=None): #return n
domain = self.get_account_addresses(account)
return tx.get_value(domain, self.prevout_values) def get_tx_delta(self, tx_hash, address):
"effect of tx on address"
def update_tx_outputs(self, tx_hash): delta = 0
tx = self.transactions.get(tx_hash) # substract the value of coins sent from address
d = self.txi.get(tx_hash, {}).get(address, [])
for i, (addr, value) in enumerate(tx.get_outputs()): for n, v in d:
key = tx_hash+ ':%d'%i delta -= v
self.prevout_values[key] = value # 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: for item in tx.inputs:
if self.is_mine(item.get('address')): addr = item.get('address')
key = item['prevout_hash'] + ':%d'%item['prevout_n'] if addr in addresses:
self.spent_outputs.append(key) 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): def get_addr_balance(self, address):
'returns the confirmed balance and pending (unconfirmed) balance change of this bitcoin address' "returns the confirmed balance and pending (unconfirmed) balance change of a bitcoin address"
#assert self.is_mine(address) h = self.history.get(address, [])
h = self.history.get(address,[])
if h == ['*']: return 0,0
c = u = 0 c = u = 0
received_coins = [] # list of coins received at address for tx_hash, height in h:
# go through all tx in history of this address and collect the coins arriving on this address v = self.get_tx_delta(tx_hash, address)
for tx_hash, tx_height in h: if height > 0:
tx = self.transactions.get(tx_hash) c += v
if not tx: continue else:
u += v
return c, u
for i, (addr, value) in enumerate(tx.get_outputs()): def get_addr_utxo(self, address):
if addr == address: h = self.history.get(address, [])
key = tx_hash + ':%d'%i coins = {}
received_coins.append(key) for tx_hash, height in h:
# go through all tx in history of this address again l = self.txo.get(tx_hash, {}).get(address, [])
for tx_hash, tx_height in h: for n, v, is_cb in l:
tx = self.transactions.get(tx_hash) coins[tx_hash + ':%d'%n] = (height, v, is_cb)
if not tx: continue for tx_hash, height in h:
v = 0 l = self.txi.get(tx_hash, {}).get(address, [])
# substract the value of coins leaving from this address for txi, v in l:
for item in tx.inputs: coins.pop(txi)
addr = item.get('address') return coins.items()
if addr == address:
key = item['prevout_hash'] + ':%d'%item['prevout_n'] def get_unspent_coins(self, domain=None):
value = self.prevout_values.get( key ) coins = []
if key in received_coins: if domain is None:
v -= value domain = self.addresses(True)
# add the value of the coins arriving in this address for addr in domain:
for i, (addr, value) in enumerate(tx.get_outputs()): c = self.get_addr_utxo(addr)
key = tx_hash + ':%d'%i for txo, v in c:
if addr == address: tx_height, value, is_cb = v
v += value prevout_hash, prevout_n = txo.split(':')
output = {
if tx_height: 'address':addr,
c += v # confirmed coins value '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: else:
u += v # unconfirmed coins value u += v
return c, u return c, u
def get_account_name(self, k): def get_account_name(self, k):
@ -508,133 +571,170 @@ class Abstract_Wallet(object):
uu += u uu += u
return cc, uu 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): def set_fee(self, fee):
if self.fee_per_kb != fee: if self.fee_per_kb != fee:
self.fee_per_kb = fee self.fee_per_kb = fee
self.storage.put('fee_per_kb', self.fee_per_kb, True) self.storage.put('fee_per_kb', self.fee_per_kb, True)
def get_address_history(self, address):
def get_history(self, address):
with self.lock: with self.lock:
return self.history.get(address) return self.history.get(address, [])
def get_status(self, h): def get_status(self, h):
if not h: return None if not h:
if h == ['*']: return '*' return None
status = '' status = ''
for tx_hash, height in h: for tx_hash, height in h:
status += tx_hash + ':%d:' % height status += tx_hash + ':%d:' % height
return hashlib.sha256( status ).digest().encode('hex') 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: with self.transaction_lock:
self.add_pubkey_addresses(tx) # 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
def receive_tx_callback(self, tx_hash, tx, tx_height):
if not self.check_new_tx(tx_hash, tx): if not self.check_new_tx(tx_hash, tx):
# may happen due to pruning # may happen due to pruning
print_error("received transaction that is no longer referenced in history", tx_hash) print_error("received transaction that is no longer referenced in history", tx_hash)
return return
self.transactions[tx_hash] = tx self.add_transaction(tx_hash, tx, tx_height)
self.network.pending_transactions_for_notifications.append(tx) #self.network.pending_transactions_for_notifications.append(tx)
self.save_transactions()
if self.verifier and tx_height>0: if self.verifier and tx_height>0:
self.verifier.add(tx_hash, tx_height) 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_history_callback(self, addr, hist): def receive_history_callback(self, addr, hist):
if not self.check_new_history(addr, hist): #if not self.check_new_history(addr, hist):
raise Exception("error: received history for %s is not consistent with known transactions"%addr) # raise Exception("error: received history for %s is not consistent with known transactions"%addr)
with self.lock: with self.lock:
self.history[addr] = hist self.history[addr] = hist
self.storage.put('addr_history', self.history, True) self.storage.put('addr_history', self.history, True)
if hist != ['*']:
for tx_hash, tx_height in hist: for tx_hash, tx_height in hist:
if tx_height>0: if tx_height>0:
# add it in case it was previously unconfirmed # add it in case it was previously unconfirmed
if self.verifier: self.verifier.add(tx_hash, tx_height) 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 = []
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
c, u = self.get_account_balance(account) # 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)
if balance != c+u:
result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) )
balance = c + u - balance def get_history(self, account=None):
for tx_hash, tx in history: # get domain
is_relevant, is_mine, value, fee = self.get_tx_value(tx, account) domain = self.get_account_addresses(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) 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.append( (tx_hash, conf, value, timestamp) )
history.sort(key = lambda x: self.verifier.get_txpos(x[0]))
return history
return result
def get_label(self, tx_hash): def get_label(self, tx_hash):
label = self.labels.get(tx_hash) label = self.labels.get(tx_hash)
is_default = (label == '') or (label is None) 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 return label, is_default
def get_default_label(self, tx_hash): def get_default_label(self, tx_hash):
return tx_hash
tx = self.transactions.get(tx_hash) tx = self.transactions.get(tx_hash)
default_label = '' default_label = ''
if tx: if tx:
is_relevant, is_mine, _, _ = self.get_tx_value(tx) _, is_mine, _, _ = self.get_wallet_delta(tx)
if is_mine: if is_mine:
for o_addr in tx.get_output_addresses(): for o_addr in tx.get_output_addresses():
if not self.is_mine(o_addr): if not self.is_mine(o_addr):
@ -702,7 +802,7 @@ class Abstract_Wallet(object):
amount = sum( map(lambda x:x[2], outputs) ) amount = sum( map(lambda x:x[2], outputs) )
total = fee = 0 total = fee = 0
inputs = [] inputs = []
tx = Transaction(inputs, outputs) tx = Transaction.from_io(inputs, outputs)
for item in coins: for item in coins:
if item.get('coinbase') and item.get('height') + COINBASE_MATURITY > self.network.get_local_height(): if item.get('coinbase') and item.get('height') + COINBASE_MATURITY > self.network.get_local_height():
continue continue
@ -860,7 +960,6 @@ class Abstract_Wallet(object):
# review transactions that are in the history # review transactions that are in the history
for addr, hist in self.history.items(): for addr, hist in self.history.items():
if hist == ['*']: continue
for tx_hash, tx_height in hist: for tx_hash, tx_height in hist:
if tx_height>0: if tx_height>0:
# add it in case it was previously unconfirmed # add it in case it was previously unconfirmed
@ -874,23 +973,22 @@ class Abstract_Wallet(object):
def check_new_history(self, addr, hist): def check_new_history(self, addr, hist):
# check that all tx in hist are relevant # check that all tx in hist are relevant
if hist != ['*']:
for tx_hash, height in hist: for tx_hash, height in hist:
tx = self.transactions.get(tx_hash) tx = self.transactions.get(tx_hash)
if not tx: continue if not tx:
continue
if not tx.has_address(addr): if not tx.has_address(addr):
return False return False
# check that we are not "orphaning" a transaction # check that we are not "orphaning" a transaction
old_hist = self.history.get(addr,[]) old_hist = self.history.get(addr,[])
if old_hist == ['*']: return True
for tx_hash, height in old_hist: 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 found = False
for _addr, _hist in self.history.items(): for _addr, _hist in self.history.items():
if _addr == addr: continue if _addr == addr:
if _hist == ['*']: continue continue
_tx_hist = map(lambda x:x[0], _hist) _tx_hist = map(lambda x:x[0], _hist)
if tx_hash in _tx_hist: if tx_hash in _tx_hist:
found = True found = True
@ -916,7 +1014,6 @@ class Abstract_Wallet(object):
print_error("sync:", ext_requests, ext_h) print_error("sync:", ext_requests, ext_h)
height = None height = None
for h in ext_h: for h in ext_h:
if h == ['*']: continue
for item in h: for item in h:
if item.get('tx_hash') == tx_hash: if item.get('tx_hash') == tx_hash:
height = item.get('height') height = item.get('height')
@ -933,7 +1030,6 @@ class Abstract_Wallet(object):
# 1 check that tx is referenced in addr_history. # 1 check that tx is referenced in addr_history.
addresses = [] addresses = []
for addr, hist in self.history.items(): for addr, hist in self.history.items():
if hist == ['*']:continue
for txh, height in hist: for txh, height in hist:
if txh == tx_hash: if txh == tx_hash:
addresses.append(addr) addresses.append(addr)
@ -996,8 +1092,6 @@ class Abstract_Wallet(object):
def address_is_old(self, address, age_limit=2): def address_is_old(self, address, age_limit=2):
age = -1 age = -1
h = self.history.get(address, []) h = self.history.get(address, [])
if h == ['*']:
return True
for tx_hash, tx_height in h: for tx_hash, tx_height in h:
if tx_height == 0: if tx_height == 0:
tx_age = 0 tx_age = 0

15
plugins/exchange_rate.py

@ -499,9 +499,9 @@ class Plugin(BasePlugin):
@hook @hook
def load_wallet(self, wallet): def load_wallet(self, wallet):
tx_list = {} tx_list = {}
for item in self.wallet.get_tx_history(self.wallet.storage.get("current_account", None)): for item in self.wallet.get_history(self.wallet.storage.get("current_account", None)):
tx_hash, conf, is_mine, value, fee, balance, timestamp = item tx_hash, conf, value, timestamp = item
tx_list[tx_hash] = {'value': value, 'timestamp': timestamp, 'balance': balance} tx_list[tx_hash] = {'value': value, 'timestamp': timestamp }
self.tx_list = tx_list self.tx_list = tx_list
self.cur_exchange = self.config.get('use_exchange', "Blockchain") self.cur_exchange = self.config.get('use_exchange', "Blockchain")
@ -572,20 +572,21 @@ class Plugin(BasePlugin):
except Exception: except Exception:
newtx = self.wallet.get_tx_history() newtx = self.wallet.get_tx_history()
v = newtx[[x[0] for x in newtx].index(str(item.data(0, Qt.UserRole).toPyObject()))][3] 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 pass
tx_time = int(tx_info['timestamp']) tx_time = int(tx_info['timestamp'])
tx_value = Decimal(str(tx_info['value'])) / 100000000
if self.cur_exchange == "CoinDesk": if self.cur_exchange == "CoinDesk":
tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d') tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
try: 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: except KeyError:
tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(str(tx_info['value']))/100000000 , "USD") tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(str(tx_info['value']))/100000000 , "USD")
elif self.cur_exchange == "Winkdex": elif self.cur_exchange == "Winkdex":
tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d') + "T16:00:00-04:00" tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d') + "T16:00:00-04:00"
try: try:
tx_rate = self.resp_hist[[x['timestamp'] for x in self.resp_hist].index(tx_time_str)]['price'] 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: except ValueError:
tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(tx_info['value'])/100000000 , "USD") tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(tx_info['value'])/100000000 , "USD")
except KeyError: except KeyError:
@ -594,7 +595,7 @@ class Plugin(BasePlugin):
tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d') tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
try: try:
num = self.resp_hist[tx_time_str].replace(',','') 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: except KeyError:
tx_fiat_val = _("No data") tx_fiat_val = _("No data")

38
plugins/plot.py

@ -42,30 +42,28 @@ class Plugin(BasePlugin):
@hook @hook
def export_history_dialog(self, d,hbox): def export_history_dialog(self, d,hbox):
self.wallet = d.wallet self.wallet = d.wallet
history = self.wallet.get_history()
history = self.wallet.get_tx_history()
if len(history) > 0: if len(history) > 0:
b = QPushButton(_("Preview plot")) b = QPushButton(_("Preview plot"))
hbox.addWidget(b) hbox.addWidget(b)
b.clicked.connect(lambda: self.do_plot(self.wallet)) b.clicked.connect(lambda: self.do_plot(self.wallet, history))
else: else:
b = QPushButton(_("No history to plot")) b = QPushButton(_("No history to plot"))
hbox.addWidget(b) hbox.addWidget(b)
def do_plot(self, wallet, history):
def do_plot(self,wallet):
history = wallet.get_tx_history()
balance_Val=[] balance_Val=[]
fee_val=[] fee_val=[]
value_val=[] value_val=[]
datenums=[] datenums=[]
unknown_trans=0 unknown_trans = 0
pending_trans=0 pending_trans = 0
counter_trans=0 counter_trans = 0
balance = 0
for item in history: 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 confirmations:
if timestamp is not None: if timestamp is not None:
try: try:
@ -73,24 +71,15 @@ class Plugin(BasePlugin):
balance_string = format_satoshis(balance, False) balance_string = format_satoshis(balance, False)
balance_Val.append(float((format_satoshis(balance,False)))*1000.0) balance_Val.append(float((format_satoshis(balance,False)))*1000.0)
except [RuntimeError, TypeError, NameError] as reason: except [RuntimeError, TypeError, NameError] as reason:
unknown_trans=unknown_trans+1 unknown_trans += 1
pass pass
else: else:
unknown_trans=unknown_trans+1 unknown_trans += 1
else: else:
pending_trans=pending_trans+1 pending_trans += 1
if value is not None:
value_string = format_satoshis(value, True) value_string = format_satoshis(value, True)
value_val.append(float(value_string)*1000.0) value_val.append(float(value_string)*1000.0)
else:
value_string = '--'
if fee is not None:
fee_string = format_satoshis(fee, True)
fee_val.append(float(fee_string))
else:
fee_string = '0'
if tx_hash: if tx_hash:
label, is_default_label = wallet.get_label(tx_hash) label, is_default_label = wallet.get_label(tx_hash)
@ -139,12 +128,9 @@ class Plugin(BasePlugin):
xfmt = md.DateFormatter('%Y-%m-%d') xfmt = md.DateFormatter('%Y-%m-%d')
ax.xaxis.set_major_formatter(xfmt) 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].plot(datenums,value_val,marker='o',linestyle='-',color='green',label='Value')
axarr[1].legend(loc='upper left') 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.annotate('unknown transaction = %d \n pending transactions = %d' %(unknown_trans,pending_trans),xy=(0.7,0.05),xycoords='axes fraction',size=12)
plt.show() plt.show()

Loading…
Cancel
Save